alloc 流程

alloc 流程

源码地址

官方源码 objc4-818.2
https://opensource.apple.com/tarballs/objc4/
可编译源码
https://github.com/LGCooci/objc4_debug
llvm 源码
https://github.com/llvm/llvm-project/releases/tag/llvmorg-13.0.1

探索方式

  • 通过汇编跟流程 找到调用 objc_alloc ,并给 objc_alloc 打符号断点
  • 给 alloc 打符号断点

通过以上两种方式,都可以得知,alloc 调用的是 libobjc.A.dylib 中的 objc_alloc 方法。libobjc.A.dylib 可从 objc 源码中进行分析。

调试源码

alloc 调用顺序

NSObject 源码中 alloc 方法

1
2
3
+ (id)alloc {
return _objc_rootAlloc(self);
}

跟进 _objc_rootAlloc

1
2
3
4
5
6
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

继续跟进 callAlloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif

// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

callAlloc 中有两个分支 _objc_rootAllocWithZoneobjc_msgSend 继续在这两个分支打断点来进行调试。
可以得到调用顺序 _objc_rootAlloc -> _objc_rootAllocWithZone -> objc_msgSend

继续上面的流程跟进 _objc_rootAllocWithZone

1
2
3
4
5
6
7
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}

再继续跟进 _class_createInstanceFromZone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
...

// 内存计算的关键代码
size = cls->instanceSize(extraBytes);

...

// 通过 calloc 为 obj 开辟空间
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}

...

if (!zone && fast) {
// isa 与 对象绑定 (关联前obj为id类型,关联后obj变为实际的类型)
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}

...
}

总结

通过源码分析,alloc 的调用顺序为:
alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> objc_msgSend

_class_createInstanceFromZone 做了如下关键操作

  1. cls->instanceSize 获取了对象的大小。
  2. 使用 calloc 给对象开辟空间。
  3. obj->initInstanceIsa 关联isa指针。

一些补充

为什么通过断点调试会发现调用到 objc_alloc,而通过源码分析确没有 objc_alloc 方法?

在llvm源码中我们可以得到答案
在llvm源码中搜索 objc_alloc 首先可看到 ObjCRuntime.h 中有这么一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// Does this runtime provide entrypoints that are likely to be faster
/// than an ordinary message send of the "alloc" selector?
///
/// The "alloc" entrypoint is guaranteed to be equivalent to just sending the
/// corresponding message. If the entrypoint is implemented naively as just a
/// message send, using it is a trade-off: it sacrifices a few cycles of
/// overhead to save a small amount of code. However, it's possible for
/// runtimes to detect and special-case classes that use "standard"
/// alloc behavior; if that's dynamically a large proportion of all
/// objects, using the entrypoint will also be faster than using a message
/// send.
///
/// When this method returns true, Clang will turn non-super message sends of
/// certain selectors into calls to the corresponding entrypoint:
/// alloc => objc_alloc
/// allocWithZone:nil => objc_allocWithZone

bool shouldUseRuntimeFunctionsForAlloc() const {
switch (getKind()) {
case FragileMacOSX:
return false;
case MacOSX:
return getVersion() >= VersionTuple(10, 10);
case iOS:
return getVersion() >= VersionTuple(8);
case WatchOS:
return true;

case GCC:
return false;
case GNUstep:
return false;
case ObjFW:
return false;
}
llvm_unreachable("bad kind");
}

通过注释就可得知,如果该方法返回 true 则:
alloc => objc_alloc
allocWithZone:nil => objc_allocWithZone
而 iOS 8 以上则会返回 true 。

接着搜索 shouldUseRuntimeFunctionsForAlloc 可看到当 shouldUseRuntimeFunctionsForAlloc 返回 true 时会调用 EmitObjCAllocEmitObjCAllocWithZone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static Optional<llvm::Value *>
tryGenerateSpecializedMessageSend(CodeGenFunction &CGF, QualType ResultType,
llvm::Value *Receiver,
const CallArgList& Args, Selector Sel,
const ObjCMethodDecl *method,
bool isClassMessage) {
...
Runtime.shouldUseRuntimeFunctionsForAlloc() &&
ResultType->isObjCObjectPointerType()) {
// [Foo alloc] -> objc_alloc(Foo) or
// [self alloc] -> objc_alloc(self)
if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
// [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or
// [self allocWithZone:nil] -> objc_allocWithZone(self)
if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 &&
Args.size() == 1 && Args.front().getType()->isPointerType() &&
Sel.getNameForSlot(0) == "allocWithZone") {
const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal();
if (isa<llvm::ConstantPointerNull>(arg))
return CGF.EmitObjCAllocWithZone(Receiver,
CGF.ConvertType(ResultType));
return None;
}
}
...
}

继续分析 EmitObjCAllocEmitObjCAllocWithZone 也验证了会调用objc_alloc 和 objc_allocWithZone 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// Allocate the given objc object.
/// call i8* \@objc_alloc(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
llvm::Type *resultType) {
return emitObjCValueOperation(*this, value, resultType,
CGM.getObjCEntrypoints().objc_alloc,
"objc_alloc");
}

/// Allocate the given objc object.
/// call i8* \@objc_allocWithZone(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAllocWithZone(llvm::Value *value,
llvm::Type *resultType) {
return emitObjCValueOperation(*this, value, resultType,
CGM.getObjCEntrypoints().objc_allocWithZone,
"objc_allocWithZone");
}

结论:
llvm在编译阶段对alloc做了hook,创建对象时第一次调用alloc方法会进入objc_alloc方法然后进入callAlloc方法callAlloc方法里通过objc_msgSend再次调用alloc方法这次调用才会走alloc的正常流程

init 方法

先看源码

1
2
3
- (id)init {
return _objc_rootInit(self);
}

继续跟入 _objc_rootInit

1
2
3
4
5
6
7
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}

alloc init 和 new 有什么区别

[[SomeObject alloc] init] 打断点,在汇编中可发现调用了 objc_alloc_init 方法,查看 objc_alloc_init 源码

1
2
3
4
5
6
// Calls [[cls alloc] init].
id
objc_alloc_init(Class cls)
{
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}

[SomeObject new] 打断点,在汇编中可发现实际调用了 objc_opt_new 方法,查看 objc_opt_new 源码

1
2
3
4
5
6
7
8
9
10
11
// Calls [cls new]
id
objc_opt_new(Class cls)
{
#if __OBJC2__
if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
return [callAlloc(cls, false/*checkNil*/) init];
}
#endif
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}

因为我们现在使用的是 __OBJC2__ ,所以也是调用 [callAlloc(cls, false/*checkNil*/) init];
结论alloc initnew 在底层是一样的。