本文源码基于objc4-781, macOS 10.15.6, Xcode Version 11.7 (11E801a)
在开发过程中我们会经常使用到 + alloc
和 - init
方法,但是一直没有深入探究过。这部分代码其实苹果是开源的,我们可以直接通过源码来探索他们的底层实现。
alloc 方法分析
id _objc_rootAlloc(Class cls)
└── static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
└── id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
└── static ALWAYS_INLINE id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct, size_t *outAllocatedSize)
├── size_t instanceSize(size_t extraBytes)
├── void *calloc(size_t, size_t)
└── inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
上面就是调用 + alloc
方法的调用栈,这里只展示了关键部分的代码。
callAlloc
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
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));
}
cls->ISA()->hasCustomAWZ()
: 是否重写了+ alloc
或 + allocWithZone
方法。
如果没有重写 + alloc
或 + allocWithZone
就会调用 _objc_rootAllocWithZone
,否则会执行消息转发流程。
-
slowpath(x)
和fastpath(x)
在条件判断语句中如果使用了 fastpath(x)
, 编译器会将在它之后的代码块紧接着前面的代码,而将slowpath(x)
后的代码块放在更远的位置,这样能够减少读取代码是由于指令跳转带来的性能损耗。因此我们一般将大概率执行的条件放在 fastpath(x)
中。
_class_createInstanceFromZone
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_t size = cls->instanceSize(extraBytes);
// 开辟内存
id obj = (id)calloc(1, size);
// 初始化cls信息
obj->initInstanceIsa(cls, hasCxxDtor);
return obj;
}
上面代码只写出了关键部分,首先计算出对象需要开辟的内存空间的大小,然后开辟内存空间,此时开辟出来的内存并没有任何内容,因此我们需要初始化isa
。
- 字节对齐:有一点需要注意的是为对象分配的内存大小是
16bytes
对齐的, 且最小为16btyes
。在以前的版本中是8bytes
字节对齐的。
但是在通过可调式的源码跟踪对象创建的过程,发现了我们调用 [Person alloc]
时,此时并不会直接执行,而是先调用了 objc_alloc
方法, 方法实现如下:
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
file: Source/NSObject.mm line: 1726
上面的注释是源码中苹果提供的,在调用alloc
的方法时,确实会调用此方法。
接着调用callAlloc
, 注意这里的第三个参数传的是 false
。在callAlloc
方法中,判断条件!cls->ISA()->hasCustomAWZ()
为 false
且 allocWithZone
也为 false
,此时会执行 return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
进入消息转发流程。
在消息转发的过程中会在查找当前cls和它的父类是否有重新+ alloc
或 + allocWithZone:
方法,如果有重写,那么 !cls->ISA()->hasCustomAWZ()
依然为 false
, 否则此条件就为 true
。
在消息转发流程中,最后会执行 +[NSObject alloc]
方法,这样就回到我们最初探讨的流程了。
这里我们来看一下 _objc_rootAlloc
方法的实现:
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
我们注意到这里第三个参数传递的是 true
, 再次执行 _objc_rootAlloc
方法,!cls->ISA()->hasCustomAWZ()
条件成立就会直接调用 _objc_rootAllocWithZone
方法, 如果不成立,接着往下执行,此时的 allocWithZone
参数为 true
就会执行 return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
, 开始 + allocWithZone:
的消息转发流程。
最终会调用 +[NSObject allocWithZone:]
, 接着调用 _objc_rootAllocWithZone
, 此时又回到了,我们开始分析的流程了。
总结: 在调用 + alloc
方法时, 底层实现并不是直接调用 + alloc
方法的,而是首先调用了 objc_alloc
方法,在对一个类第一次进行初始化的时候 !cls->ISA()->hasCustomAWZ()
条件总是为 false
, 然后进入 alloc
的消息转发流程, 同时查找当前类和他的父类是否重写了 + alloc
或 + allocWithZone:
方法,如果没有找到那么 !cls->ISA()->hasCustomAWZ()
条件就会变为 true
。 通过 +[NSObject alloc]
再次调用 callAlloc
方法,如果此时 !cls->ISA()->hasCustomAWZ()
为 true
就会直接进行初始化对象。
否则就会进入+ allocWithZone:
的消息转发流程,最终通过 +[NSObject allocWithZone]
进行对象的创建。
笔者认为苹果这样做是为了进行性能优化,在开发的过程中+ alloc
可能是我们调用最多的方法之一,如果一个类没有重写+ alloc
和 + allocWithZone:
方法,实际上它就没有必要进入消息转发流程,可以直接进行对象的内存空间开辟,这样做避免了消息转发带来的性能损耗。
init 方法分析
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
return obj;
}
init
方法实现就相当的简单,直接将self
返回。这样做的目的实际上就是为子类提供一个工厂方法,让子类 做自定义的初始化操作。