Alloc的底层原理

227 阅读8分钟

前言

在objc里我们创建对象的时候会经常的用到alloc和init,那么objc在调用alloc的时候它的底层到底是进行了什么样的处理呐?

在开始前先运行下面这个demo

    Person *p = [Person alloc];
    p.age = 20;
    Person *p1 = [p init];
    Person *p2 = [p init];
    NSLog(@"\n%@\n%@\n%@\n %p\n %p\n %p\n ",p,p1,p2, &p,&p1,&p2);
    NSLog(@"%d %d",p1.age,p2.age);

其运行结果如下:

很明显可以看到p,p1,p2一样,而&p,&p1,&p2不一样,为什么会这样呐?

读者可以参考下面图配合这段文字进行理解:

我们通过Person alloc创建出的p,其实是在堆区创建了一个p的内存空间0x600003588500,而&p是栈区的指针,它指向0x600003588500,p init创建了同样指向0x600003588500的指针&p1和&p2, 所以可以看到可以看到p,p1,p2的属性age都是一样的,他们是同时指向一个地址0x600003588500。

\

1、汇编分析

开始汇编分析前,给读者提下几个汇编的指令:

  • b,bl 跳转指令表示函数的调用
  • ret 函数返回; 注释
  • ;来代表注释

还有一点知识:cpu分为寄存器、运算器、控制器,

cpu先从内存里读数据保存到寄存器中再进行运算,这样效率会高

1.1汇编分析过程

先在xcode工程打开汇编模式,Debug ->Debug Workflow ->Always Show Disassembly,如图:

设置好汇编模式后,在这打个断点,编译就会进入汇编指令

这里很熟悉地看到一个bl指令,这是一个函数的跳转指令,";"号后面是注释,告诉我们这里调了objc_alloc这个函数,接着我们给objc_alloc打个断点,进入objc_alloc函数中,如下图,想了解怎么打断点可以参考文末附录。

可以看到,出现了两个函数objc_msgSend和_objc_rootAllocWithZone,这是我们可以通过点击control键+下箭头来一步一步地走汇编指令,发现跳过_objc_rootAllocWithZone,执行了objc_msgSend,当汇编走第11行到时候,感兴趣的读者可以通过lldb读下x0和x1寄存器(register read x0),看看他们当前是什么,演示如下图:

po (char *)是强转的意思,可以看到x0是person而x1是alloc方法

上面这个图告诉我们

这个objc_msgSend函数其实就是通过Person去调alloc方法

接着我们给[NSObjc_alloc]打个断点,

alloc里有一个_objc_rootAlloc,接着进入到这个函数里

这个时候又看到了两个方法_objc_rootAllocWithZone和objc_msgSend,接着走汇编指令,可以看到它跑到了_objc_rootAllocWithZone这个方法里来了,到了这一步,就得去到我们的源码里再进行近一步的分析了,读者可以移步阅读下文的源码分析,这个方法返回的是一个id的类型,是_class_createInstanceFromZone的返回值。

可以进入_objc_rootAllocWithZone看看里面实现了什么

这里就运用到了ret指令了,在这打个断点,然后通过lldb来查看x0寄存器里是什么

1.2 汇编分析的结论

通过上面的分析,编者给大家做个小总结:

alloc是真正给我们创建实例对象的方法

alloc方法的大概流程是这样的:

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

看着这个结论大家可以吐槽下,分析了这么久就只得到这么一个函数之间的跳转,没有实际意义,其实拿到这个流程后对我们去分析源码是有很大助力的,当然如果你的汇编能力很强,可以继续推下去,核心代码在_class_createInstanceFromZone,通过汇编你能更了解其底层到底做了什么,但可能有部分会被编译器给优化掉了,所以建议到了这步可以去分析源码了。

2、源码分析:

在进行源码分析前,读者可以到官网去下载objc里的alloc源码,这是链接,可以找最新版的objc4-838.1

由于已经用汇编演示了alloc底层大致流程了,源码这里就不断点分析其流程了,直接分析每个方法实现了什么,最核心的方法是_class_createInstanceFromZone,前面的方法都是一些跳转和判断。

2.1 objc_alloc

源码如下:

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

这个callAlloc方法我们在汇编没有看到是因为编译器的优化,可以点进去看到看下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_rootAllocWithZone和objc_msgSend这两个函数,第一次从objc_alloc进来是先跑到objc_msgSend,因为这个时候checkNil为true,allocWithZone为默认的flase,所以fastpath(!cls->ISA()->hasCustomAWZ())中的cls是还没有指针isa,所以为是flase,因此第一次进来跑了((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)),这个方法会触发objc_msgSend调用initialize类方法,所以第二次会跑到_objc_rootAllocWithZone里去

2.2 第二次调callAlloc

通过第一次的callAlloc的返回,然后打断点会发现再一次进入到callAlloc

第二次调callAlloc的时候,checkNIl是false,由于第一次调用objc_msgSend调了initialize方法,这时到bist是有值了,所以cls上是有了指针,那么hasCustomAWZ()这个方法是用来判断是否实现allocWithZone的自定义,可以看下这个方法;

bool hasCustomAWZ() {
        return ! bits.hasDefaultAWZ();
}

这个allocWithZone是可以自定义的如果没有自定义,那么就会调用系统默认的allocWithZone方法。

综上,我们知道第二次调callAlloc程序会跑到_objc_rootAllocWithZone(cls, nil)里去,那我们去看下这个方法里实现了什么

2.3 _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);
}

其实就是返回_class_createInstanceFromZone(cls, 0, nil,OBJECT_CONSTRUCT_CALL_BADALLOC)返回值。

2.4 _class_createInstanceFromZone

源码:

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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
    // 判断当前的class或supclass是否有.cxxConstruct构造方法的实现
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    // 判断当前的class或supclass是否有.cxxDestruct构造方法的实现
    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) {
        //初始化实例的isa指针
        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.5结论

通过断点来进行源码的的解读,可以得到alloc的具体底层是这么一个流程:alloc->_objc_rootAlloc->callAlloc(cls,false,true)->allocWithZone->_objc_rootAllocWithZone->class_createInstance->_class_createInstanceFromZone

其中 _class_createInstanceFromZone比较核心的代码,下面我们来将这部分进行详细探索


3、init和new

init的内部其实是什么都不做,只是返回了一个self

这个是一种工厂模式,为了让你重写这个init方法,可以在init内部进行一些初始化的操作。

那么new方法其实就是alloc+init,这里就不多讲了

4、内存对齐

当我们创建对像的时候,肯定得给对象分配内存空间

_class_createInstanceFromZone里的这一句代码就是进行内存的对齐和分配

cls->instanceSize(extraBytes)

instanceSize方法的源码:

可以看到程序来到了word_align(unalignedInstanceSize())

就是x加上7再与上一个非7,

解释:比如一个数:0011, 7的二进制是0111, 7的取反是1000

首先加上7: 0011 + 0111 = 1010

再与上7的取反 1010 & 1000 = 1000 这一步相当于给前3位清0

最终得到的就是8的二进制1000,这个时候是不是就是对齐到8了,不信的话可以换个数自己来试试。

这个时候4位是不是1000,那个就是8。

效果可以这么理解:(x+7)>>3<<3就是x加上7然后在左移3再右移3

好了现在弄懂什么是内存对齐后,我们继续回到源码

这里有个cache说明这里有个缓存,进入fastInstanceSize(extraBytes)

这个align16就是16字节对齐的意思

这里就又个问题了,有缓存的时候是以16 字节对齐的,但没缓存的时候是以8字节对齐的,那么我们alloc方法是以16字节对齐还是8字节对齐的呐?答案在下问。

5、alloc的内存原理

上面了解了内存对齐后,我们在另一个源码libmalloc里去研究下allco的内存是怎么分配的,这个源码和苹果可能是不会完全重合的。

这个_malloc_zone_calloc方法层级比较深,不细讲这个,可以全局搜索_nano_malloc_check_clear

有个segregated_size_to_fit方法,里面其实就是我们前面讲到的字节对齐的算法来的,其中NANO_REGIME_QUANTA_SIZE这个变量是16 ,因此是一个16字节的对齐。现在可以回答前问的一个问题了,

alloc的创建对象是以16字节对齐的!

6、编译器的优化

我们在分析汇编的时候是没有看到 _class_createInstanceFromZone这个方法的,其实是编译器进行了优化得到才出现这种情况。可以进行下图的操作进行设置编译器的优化等级,越往下优化等级越高