⭐ 表示「核心逻辑内容」
? 表示「分支知识扩展」
表示「苹果官方文档或者源码」
OC对象初始化流程
本文主要内容分三个部分:1、OC对象alloc
的流程图 2、重点分析核心函数_class_createInstanceFromZone
主要的三个功能:计算对象大小、申请内存及初始化ISA、3、alloc
之后的 init
以及new
方法的底层分析。
资源:
objc4-源码
可编译的 objc4 源码 (Cooci大神)
1、OC对象alloc的流程图
⭐ 一个OC子类调用 alloc
流程图如下
首先搭建调试代码:
断点定在第一个 [MGObject alloc]
断住之后,打开反汇编调试页面(Xcode工具栏「Debug」->「Debug workflow」-> 「Allways show disassembly」)
可以看出汇编指令 callq
调用的函数是 objc_alloc
,也就是流程图的第一步, 接下来查看 NObject.mm
文件里
⭐ objc_alloc
函数源码:
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
复制代码
⭐ 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));
}
复制代码
可以看出objc_alloc
直接调用 callAlloc
, 参数传 cls, true/*checkNil*/, false/*allocWithZone*/
,callAlloc
的关键判断代码是 !cls->ISA()->hasCustomAWZ()
,看字面意思是判断cls 有没有自定义的 allocWithZone 方法
? hasCustomAWZ()
源码:
#if FAST_CACHE_HAS_DEFAULT_AWZ
bool hasCustomAWZ() const {
return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
void setHasDefaultAWZ() {
cache.setBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
void setHasCustomAWZ() {
cache.clearBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
#else
bool hasCustomAWZ() const {
return !(bits.data()->flags & RW_HAS_DEFAULT_AWZ);
}
void setHasDefaultAWZ() {
bits.data()->setFlags(RW_HAS_DEFAULT_AWZ);
}
void setHasCustomAWZ() {
bits.data()->clearFlags(RW_HAS_DEFAULT_AWZ);
}
#endif
复制代码
可以看出 hasCustomAWZ()
是对 HAS_DEFAULT_AWZ
取反, HAS_DEFAULT_AWZ
不存在有两种情况:1、cls
还没有被 initialized
2、自定义了(重写了)方法 allocWithZone
而在这里明显是第一种情况,重写 MGObect +(void)initialize
方法可以看到代码走了 +initialize
之后(+initialize
是在类第一次调用objc_msgSend
触发)
!cls->ISA()->hasCustomAWZ()
就返回YES
了 (HAS_DEFAULT_AWZ
是YES
,取反得到hasCustomAWZ()
的值是 NO
整体再取反得到if判断的值 YES
、、、好绕好绕_(:з」∠)_ )
第一次进来由于objc_alloc
直接调用 callAlloc
参数传的是 false/*allocWithZone*/
会触发 objc_msgSend)(cls, @selector(alloc)
这里才是真正调用 NSObject
alloc
方法的 IMP
⭐ + (id)alloc
函数源码:
+ (id)alloc {
return _objc_rootAlloc(self);
}
复制代码
? 关于为什么苹果调用alloc
为什么会先调用 objc_alloc
而不是alloc
本身的IMP
呢?可以查看有梦想的程序员的这篇文章 关于alloc初探中alloc进入objc_alloc的原因。
苹果这个处理的目的笔者认为是为了在真正初始化分配内存前保证cls
已经是被initialized
的状态而且不是nil
⭐ _objc_rootAlloc
源码
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
复制代码
接下来 _objc_rootAlloc
调用了 callAlloc
, callAlloc
第二次被调用,但是参数不一样了 cls, false/*checkNil*/, true/*allocWithZone*/
,不需要非空检查,而且需要allocWithZone,说明前置处理已经做完,要真的开辟内存初始化了。
而这次再进来由于cls
已经是 initialized
状态 代码进入 !cls->ISA()->hasCustomAWZ()
条件判断,执行_objc_rootAllocWithZone(cls, nil)
⭐ _objc_rootAllocWithZone
源码
NEVER_INLINE
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);
}
复制代码
_objc_rootAllocWithZone
直接调用 _class_createInstanceFromZone
, _class_createInstanceFromZone
是真正做事的函数,会在这个函数里计算对象大小、申请内存、初始化ISA,并返回初始化好的obj对象,这个方法的内容在下一章节展开分析
2、计算对象大小、申请内存及初始化ISA
计算对象大小、申请内存、初始化ISA,并返回初始化好的obj对象主要使用过 _class_createInstanceFromZone
函数完成,值得注意的是 _objc_rootAllocWithZone
调用_class_createInstanceFromZone
的时候 size_t
传递的是0
,void *zone
传递的是nil
⭐ _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)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
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);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
复制代码
2.1 计算对象大小
从上述源码可以看到计算大小是通过 size = cls->instanceSize(extraBytes)
计算,而extraBytes
在之前调用_objc_rootAllocWithZone
传递的是0
⭐ setInstanceSize()
相关源码
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
复制代码
fastpath路径相关源码
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
uint16_t sizeBits;
// Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
// to yield the proper 16byte aligned allocation size with a single mask
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
sizeBits &= FAST_CACHE_ALLOC_MASK;
if (newSize <= sizeBits) {
newBits |= sizeBits;
}
_flags = newBits;
}
void setInstanceSize(uint32_t newSize) {
ASSERT(isRealized());
ASSERT(data()->flags & RW_REALIZING);
auto ro = data()->ro();
if (newSize != ro->instanceSize) {
ASSERT(data()->flags & RW_COPIED_RO);
*const_cast<uint32_t *>(&ro->instanceSize) = newSize;
}
cache.setFastInstanceSize(newSize);
}
复制代码
? inline
是内联函数的关键字,告诉编译器这个函数建议编译的时候当做内联,不用开辟新的函数栈,相当于直接把代码实现放在其他函数体内,对小函数很高效,但是最终编译器会不会当做内联还得看具体代码实现,比如递归、函数体很大即便标记了inline
也会内联,编译器还是挺聪明的嘛~ 有的编译器会对尾递归
优化,反汇编后可以看到编译器把尾递归
做成了迭代函数
,没有重新callq
开辟新的函数调用栈,而是jeq
等迭代条件判断指令,如果有递归的场景最好写成尾递归
不会信开辟函数调用栈。
⭐ 上述源码分析,fastpath
路径先判断有没有cache.fastInstanceSize
有的话直接用该值, 而在cache.fastInstanceSize
是通过 setInstanceSize
–> setFastInstanceSize
方法赋值,在setFastInstanceSize
里做了次属性对齐word_align(newSize)
在最终调用 fastInstanceSize
return
之前又进行了一次 16字节对齐。
如果fastpath
没有的话会走普通的计算,调用alignedInstanceSize
方法,而这个函数的实现是直接调用属性对齐 word_align(unalignedInstanceSize())
传入参数是unalignedInstanceSize()
,这个值直接取的是存放在ro
里的instanceSize
ro()->instanceSize
两种路径都是对数据大小都进行 word_align
,具体对齐的值取决于 WORD_MASK
,而WORD_MASK
取决于编译器定义的__LP64__
意思是long、pointer类型占64位 8字节
#ifdef __LP64__
# define WORD_MASK 7UL
#else
# define WORD_MASK 3UL
#endif
复制代码
⭐ 对齐的代码是位运算向上取整在 __LP64__
的编译条件下是 8字节对齐
(x + WORD_MASK) & ~WORD_MASK
复制代码
前面的加法相当于 8 23二进制的第三位 若大于8的整数倍就进1, 后面 & ~ 相当于 & 后三位全0,其他位全1 目的是只抹零后三位,最终得出进位后的8的整数倍。
2.2 申请内存
有了上一步计算出来的 size_t
接下来就会去向系统申请内存, 由于参数zone
传的是nil
会直接走obj = (id)calloc(1, size)
, 关于 calloc
的详细说会在后续博客中详细补充。
到这已经得到obj
指向的内存地址, 但这块地址还没有被使用,首地址指向的是一片脏内存,这时候打印obj
应该是一堆乱七八糟的东西
2.3 初始化isa
在上一步,obj
已经向操作系统申请好内存空间,但是首地址也就是 Class
结构体的第一个变量 isa
还没有被初始化,首地址指向的还是一片脏内存,在isa
初始化完成后 obj
首地址 isa
被初始化成功关联到MGObject
, 关于isa及其初始化
的详细说会在后续博客中详细补充。
if (!zone && fast) {
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);
}
复制代码
最后判断有没有 c++ 的构造函数,没有就直接返回 obj
,有的话进行c++构造函数的相关逻辑后返回 obj
至此,alloc
流程结束。
3、init new 方法底层
⭐ -init
相关源码:
- (id)init {
return _objc_rootInit(self);
}
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;
}
复制代码
-init
源码很少,直接返回对象自己,有点类似c++的虚函数,真正实际的函数功能需要子类根据自身需求去实现,这里做层接口,工厂设计模式。
⭐ -new
相关源码
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{...}
复制代码
-new
的源码实现也非常精简,直接连续调用 C++函数callAlloc
返回后obj
,obj
调用OC方法-init
, 这里要注意的是如果有些类的构造方法不是 -init
的话使用 -new
可能会出问题。
? callAlloc
是c++有默认值的可选参数函数,最后一个参数 allocWithZone
默认是false
,在调用的时候可以不写这个参数,值得注意的是c++的默认参数必须从右往左连续,不能中间有没默认值的空档,目的是为了避免出现函数调用二义性。
写在最后
文章的代码环境基于苹果 objc4-818.2 源码、Xcode12,内容尽量做到了结构化、精炼,以便节省读者阅读时间成本,如有哪里书写不对或者补充欢迎及时交流沟通~~
最后,感谢您的阅读 一切祝好哈 have a nice codding (^o^)/~