第一节—从alloc函数开始探究OC

本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。

用OC开发已经4年了,虽然现在swift的确很好用,但是因为很多的项目还是以OC为基本来搭建架构的,而且swift也是在OC的思想基础上优化的,所以对OC的学习总结还是很有必要的。

想了一下,决定从alloc函数开始探究OC的思想,因为最开始的面相对象,都会用到[[Class alloc] init]。那么一个实例的出现到底经历了什么,这就需要去探索一下。

本文可能会用到一些汇编语言,具体的汇编语言可以自行某歌或某度。

一、alloc的作用

首先,先创建好一个project,并且创建一个继承于NSObject的类,用这个类的初始化来观察一下,alloc到底是什么作用,例如创建一个JDPerson类,并在controller里面初始化。

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    JDPerson *p = [JDPerson alloc];
    
    JDPerson *p1 = [p init];
    
    JDPerson *p2 = [p init];
    
    NSLog(@"p的内存地址 : %@ \n p的指针地址 : %p",p,&p);
    NSLog(@"p1的内存地址 : %@ \n p1的指针地址 : %p",p1,&p1);
    NSLog(@"p2的内存地址 : %@ \n p2的指针地址 : %p",p2,&p2);
    
    // Do any additional setup after loading the view.
}

执行结果如下图1.1所示 :


1.1.png

可以明显的发现,三个JDPerson对象的内存地址是一样的,而指针地址是不一样的,那么这也就代表了alloc只申请了一块内存空间,init分配了三个指针空间,但是三个指针全部都指向了alloc申请的内存空间。

如图1.2所示:

1.2.png

也就是说,如果这个JDPerson类含有属性的话,以这种方式初始化出来的对象,设置任意一个对象,都会让其他对象的属性发生变化,因为他们全部都指向同一块内存地址。

所以,JDPerson的对象实际上是在alloc的时候创建出来的。那么为什么又要一个init呢?原因很简单啊,init可以让我们diy啊,也就是可以重载重写override

但是本节不以init为重点,所以继续探究这个alloc是如何创建出来了一个对象的呢?

二、alloc如何创建出来的对象

需要一下alloc的源码。这是我上一节扩展配置好的,有需要的可以直接下载使用。

那现在来看一下,alloc到底是如何实现的,它在OC中的目的到底是什么。

首先随意的创建一个NSObject的子类。

JDPerson *p = [JDPerson alloc];

我们commond进去,可以看到

+ (id)alloc {
    return _objc_rootAlloc(self);
}

但是实际上第一步并不是直接调用了这个,而是调用了objc_alloc,这个会在后面说明。

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

继续下探,

比较明显的是C的代码,调用了callAlloc,传入了一个cls对象。继续进入

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

这里就很明显了,有三个判断,首先搞清楚slowpath是什么fastpath又是什么。

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

两个宏,全都是参数传入了__builtin_expect(a,b)函数,这个函数可以自行查找,意义是a == b的概率更大。所以,slowpath基本上是走不成了,因为checkNil传进来就是false

那么看fastpathfastpath检查了cls这个类或者它的父类是否有自定义alloc或者allocWithZone的实现,这个AWZ就是Alloc With Zone的首字母缩写。如果有的话,那么就将进入_objc_rootAllocWithZone

如果allocWithZone,也就是callAlloc传进来的第三个参数是true,并且,这个类没有自定义,或者它的父类也没有自定义实现allocWithZone,那么就会调用objc_msgSend发送cls这个类需要实现allocWithZone

最后无论如何,前面的条件全都不符合了,那么就会直接objc_msgSend消息,直接调用alloc

那么这段代码的整体意思,也就大概是这样子。

我们需要从中挑出来,更核心的代码,一个是_objc_rootAllocWithZone,一个是objc_msgSend

那么找那个可以继续跟入实现的函数_objc_rootAllocWithZone进去看。

三、内部的实现

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

继续跟入,

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

这里发现了,出现了一个obj对象。而且发现返回的函数object_cxxConstructFromClass有使用这个obj,那么给它上个断点,来看一下,它的变化。

下面这张图,是我给obj下了断点后,向下step了几步之后,obj出现了值。

3.1.png

这时候确定这是一个NSObject,但是,它是不是我的JDPerson,我还不知道,因为它只是开辟了一个内存空间,这也只是内存空间的地址。

但是,在这里,我们可以看到一个isa。如果想要内存地址和你的类关联,那么其中就需要存在着isa指针,来表达这次的指针的指示关系。

到这里,已经可以确认,alloc申请了内存,具体给了谁,现在还不知晓,但是这个isa表明了它有指示对象。

但是,alloc申请了内存空间,这是确认的了。而且,如果自己实践一下会发现,obj是在你step进入到calloc以后才有了内存地址的。而isa是你在step进入到initInstanceIsa的时候出现的。

所以,可以确定的是,alloc是利用了calloc分配到了内存,利用initInstanceIsa创造出来的isa指针与这块内存发生了联系,确认了类的详细归属。

四、alloc申请的内存

上面说了,obj利用calloc申请了内存,但是这个内存申请多少,是谁告诉它的?这就需要继续根据刚才的代码来看,代码我就不重复贴一大段了,贴需要看的地方。

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

这里calloc动态申请了1个长度为size的内存空间,这个size在开始就定义了size_t size,并且通过cls->instanceSize(extraBytes),获得了数值。这里也可以直接挂上断点来看,的确是拿到了需要的内存大小。

那么也就是说,alloc能拿到多少的内存空间,看的是instanceSize给它分配了多少的内存空间,所以我们就需要看一下instanceSize的源码。

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;
    }

源码中的注释明确的表明了一点,CoreFoundation的所有的对象,至少都要给16字节大小的空间。

从头来看,思路很简单,内存里面能拿到需要申请的这么大的内存的话,那就直接返回内存里面有的。

如果没有的话,出现了一个函数alignedInstanceSize(unalignedInstanceSize()),实例对齐。

将没有对齐的实例传进去,这个函数会帮我们对齐。

没有对齐的实例的源码:

uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

这里不多说这个,知道ro是当前这个类的对象里面,所有的ivarList,methodList,PropertyList等等这些list编译进去的属性的大小就行了。

那为什么要对齐,可以直接看这一篇扩展1

4.1.png

内存和isa的绑定

obj->initInstanceIsa(cls, hasCxxDtor);

这一行代码将cls和刚才的isa做到了绑定,从而obj开始知道了它是JDPerson类。

你可以在第一次运行之后再打一个断点到我打断点的位置,应该走的就是从缓存区里面拿到你的size,走的就是alloc进来的时候的_objc_rootAlloc,而不是objc_alloc了,因为你可以看到

图片.png

不再进行第二次的c++的绑定了。

那这个时候,已经是开始为你创建的对象分配内存,并且将isa和分配的内存进行绑定了。

那第二次再进来的时候,你的obj已经是JDPerson或者JDMan(我自己新建的,和JDPerson一样的,只是改了个名字)。

图片.png

最后上一下alloc的实际流程图

图片引自。他写的更为详细。

4.2.png

五、关于init

其实init的源码就很简单了。

+ (id)init {
    return (id)self;
}


- (id)init {
    return _objc_rootInit(self);
}

类方法直接返回了自己,而实力方法则是调用了_objc_rootInit

那我们继续看_objc_rootInit

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的意义何在?其实很简单,就是为了让你自己可以对他进行自定义的操作,也就是我们经常说的,重写。

你可能感兴趣的:(第一节—从alloc函数开始探究OC)