iOS底层探险 OC对象初始化流程

⭐ 表示「核心逻辑内容」

? 表示「分支知识扩展」

 表示「苹果官方文档或者源码」

OC对象初始化流程

本文主要内容分三个部分:1、OC对象alloc的流程图 2、重点分析核心函数_class_createInstanceFromZone主要的三个功能:计算对象大小、申请内存及初始化ISA、3、alloc之后的 init 以及new方法的底层分析。

资源:

objc4-源码

可编译的 objc4 源码 (Cooci大神)

1、OC对象alloc的流程图

⭐ 一个OC子类调用 alloc 流程图如下

OC对象初始化流程.png

首先搭建调试代码:

断点定在第一个 [MGObject alloc] 断住之后,打开反汇编调试页面(Xcode工具栏「Debug」->「Debug workflow」-> 「Allways show disassembly」)

截屏2021-06-06 上午8.40.49.png

可以看出汇编指令 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_AWZYES,取反得到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^)/~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享