OC底层原理1之alloc、new、init底层探索

236 阅读5分钟

alloc探索

查看代码

    Person *p1 = [Person alloc];
    Person *p2 = [p1 init];
    Person *p3 = [p1 init];
    NSLog(@"-----%@------%p----%p",p1, p1, &p1);
    NSLog(@"-----%@------%p----%p",p2, p2, &p2);
    NSLog(@"-----%@------%p----%p",p3, p3, &p3);

输出打印

-----<Person: 0x600001ba0840>------0x600001ba0840----0x7ffee79dd0e8
-----<Person: 0x600001ba0840>------0x600001ba0840----0x7ffee79dd0e0
-----<Person: 0x600001ba0840>------0x600001ba0840----0x7ffee79dd0d8

从上面可以看出p1,p2,p3的地址是在栈区,Person的类的内存地址是在堆区,而且alloc是向系统申请开辟内存的,init并没有做任何开辟申请内存的事。至于new的区别,先在前文直接指出来,可以认为它的作用就是 alloc,init的结合。

系统执行alloc方法都做了什么

我们在查看alloc方法的时候,可以看到系统并没有开源,但是要深究底层,1是查看源码,2就要借助汇编。

汇编探索(失败的过程)

(补充在lldb中打印si代表的是单步调试) 注意:该流程是第一次alloc的流程,第二次alloc是不一样的,后面不会再继续探索,但会指出来

1.我们直接断点到alloc位置的时候可以看到汇编首先调用了一个函数objc_alloc image.png 2.查看其执行的函数内部看到其调用了一个符号绑定的函数dyld_stub_binder。但是符号绑定跟我们要查看的并没大联系,所以直接从该函数内部跳过 image.png 3.终于进来objc_alloc函数内部,又看到它里面进行了判断,是跳转_objc_rootAllocWithZone函数还是跳转objc_msgSend函数,所以我们都要打上断点进行查看 image.png 4.通过断点可以看到其实它走的是objc_msgSend函数,而且它传了一个参数为alloc的在rsi寄存器中,我们先直接指出来如果继续用汇编进行探索,你可以看到它里面执行的函数太多太多,根本就不知道到底跳转到什么地方。(你在里面经过一次次的单步又发现执行的函数不知道是什么离我们的预期越来越远) image.png 终于跳转到一个_objc_msgSend_uncached函数 image.png

然后在_objc_msgSend_uncached里又看到他又去执行了lookUpImpOrForward函数,进去后更让人崩溃,已经迷失了,就不再展示所以我们要另辟蹊径了 image.png

objc源码调试alloc具体流程

注意:这个也是Person第一次alloc的流程,第二次跟第一次不一样

下载源码objc,进行查看。而objc编译项目就先不发出来了。在项目中我们创建一个Person类,然后调用alloc函数,进行查阅

1.直接看到源码实现,下面是先一步步展示源码是怎样的,但是具体流程并不是如此

// alloc实现
+ (id)alloc {
    return _objc_rootAlloc(self);
}
//  _objc_rootAlloc函数
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
// callAlloc函数
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    //可以看到汇编调试说是进入_objc_rootAllocWithZone还是objc_msgSend就是因为如此
    //但是问题是为什么又要再发送一遍alloc消息呢,那样岂不是没完没了吗,正式流程会开始说明
#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_rootAllocWithZone函数
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);
}

以下函数对于alloc是重要的,它做了三件事

  1. 通过instanceSize函数计算需要开辟的内存大小
  2. 通过 calloc函数去开辟内存得到内存地址
  3. 通过 initInstanceIsainitIsa去关联内存地址以及对象得到<类名:内存地址>
// _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;
    // 1.计算对象需要的内存大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    //苹果已经废除根据zone进行开辟内存方式
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 2.去开辟内存
        obj = (id)calloc(1, size);
    }
    // 如果说badAlloc的时候,也就是没有成功开辟内存的时候。
    //该情况很少发生
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }
    // 3. 进行内存地址与cls对象关联  <类:内存地址>
    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);
}

当我们没有进行调试的时候已经能找到的源码大致如此。当我们断点alloc _objc_rootAlloc的时候感觉流程也跟源码一样了,但是果真如此吗。如果说再加入一个断点callAlloc内部的时候就会发现。callAlloc竟然比alloc执行的还要更早,这就郁闷了。然后我们想起之前汇编的时候是不是调试流程它最先执行的是objc_alloc函数,

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

然后我们开始断点调试,最后论证的结果竟然是这样的

objc_alloc -> callAlloc -> objc_msgSend -> alloc -> _objc_rootAlloc ->
callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone

这是为什么呢,明明执行的是alloc方法却又变成objc_alloc了。我们记得汇编调试的时候有一个参数值为alloc存放在寄存器里面了,然后再执行了objc_msgSend函数,它的作用是什么,是发送消息,肯定要传alloc这个字符串过去。其中的探究过程太复杂,就直接先展示objc源码中的原因,发现苹果在程序员每次调用alloc方法的时候改变了其方法指向,是objc_alloc。这就是每次我们会先调用objc_alloc原因。 image.png

补充

1.其实苹果用的编译框架LLVM会对我们调用的alloc方法以及allocWithZone进行hook拦截,他做了类的标记,所以这就是为什么走objc_msgSend函数,之后第二次有了类的标记,就不再走标记流程了。

2.LLVM有一个特殊方法hook拦截,在编译阶段就会进行拦截方便他做分析,可以理解为我们平时APP中埋点统计一样。

Person第二次alloc流程

(alloc方法指向转变) -> objc_alloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone

init 的底层实现,看出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;
}

new 的底层实现,相当于 [alloc init]

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}