一:对象原理之alloc & init 探究

367 阅读6分钟

今天的主题就是研究allocinit,大家平时大多都是这样写的 [XXX alloc] init],那么我们从何入手呢?当然就是这里面的alloc。开篇之前先介绍下如何跟踪调试的小技巧

一: 跟踪调试技巧

1.1 断点 control + step into

最后会来到这里,这样就很明朗了, objc_alloc 方法的实现在 libobjc.A.dylib 这个动态库中。

1.2 符号断点

既然是alloc,那一定会走alloc这个方法的 当断点走到WYPerson *personA = [WYPerson alloc];我们下一个符号断点

最后也会来到这里,其实在alloc内部就是调用了rootAlloc

1.3 汇编

用真机进行调试 去掉符号断点,走到WYPerson *personA = [WYPerson alloc]; 选择Xcode菜单中 Debug - Debug Workflow - Always Show Disassembly

反正我不懂汇编,但是我能找到我懂的 [ViewController viewDidLoad] objc_alloc

我们再objc_alloc打个断点之后往下走,走到这

继续control + step into,最终还是会走到这里,libobjc.A.dylib`objc_alloc

延伸: register read 可以读取寄存器

二:alloc

2.1 alloc

alloc方法点进去,一脸懵逼,并没有具体的实现,我们知道alloc会申请内存并返回,那我想知道alloc做了什么东西,我该怎么做呢?

大家参考这个objc4-750编译,打开工程之后我们直接还是新建一个WYPerson的类,在main.m 初始化一个对象并且打印,发现的确返回了一个对象,那alloc到底做了什么呢,我们来跟一下代码 alloc中会调用 _objc_rootAlloc

但是,但是真的如此吗?来,见证奇迹的时刻!
通过之前介绍的跟踪代码的技巧我们发现会走到objc_alloc

空口无凭,你肯定不信,明明走的_objc_rootAlloc方法呀,我们来跟源码看下。找到objc_alloc方法,打上断点,我们静静的等待

我们看到的确到了objc_alloc,经过多次验证,这个方法只走一次,在这里又调用了callAlloc,就跟下面介绍的在_objc_rootAlloc中调用 callAlloc一样了,但是问题在于为什么就走一遍呢?在源码里面找到这么一段,个人猜想在编译阶段,判断alloc,然后进行符号绑定,将IMP指向objc_alloc,这些都是底层处理的,而第二次走alloc方法才是正常走的消息转发!

_objc_rootAlloc中会调用 callAlloc

这里贴上callAlloc的代码,并加上相应的注释

callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    /**
     fastpath(x)表示x很可能不为0,希望编译器进行优化;
     slowpath(x)表示x很可能为0,希望编译器进行优化——这里表示cls大概率是有值的,编译器可以不用每次都读取 return nil 指令
     */
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    /*
    实际意义是hasCustomAllocWithZone——这里表示有没有alloc / allocWithZone的实现(只有不是继承NSObject/NSProxy的类才为true)
    */
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        /*
                 内部调用了bits.canAllocFast后分为2种情况
                if(FAST_ALLOC){
                  }else{
                     默认为false,
                 }
                 当我们查看FAST_ALLOC宏时,发现它的上方有个else 1 之后才是else中定义这个宏,
                也就是说这个宏一直是不存在的
                可以自行点击进入查看
                 */
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

通过断点我们发现,fastpath(cls->canAllocFast())是不会走的,最终会来到class_createInstance的方法。从方法名上大概能看出这个方法就是是创建一个实例的,而它确实返回了一个对象给我们,下面我们来对class_createInstance进行相关的探究

2.2 class_createInstance方法探究

进入class_createInstance方法内部,发现它调用了一个_class_createInstanceFromZone(cls, extraBytes, nil)方法

id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

我们继续进入_class_createInstanceFromZone(cls, extraBytes, nil)方法内部,并贴上关键代码

在上面截图中,通过断点发现,当obj = (id)calloc(1, size);这句走完的时候,的确返回给我们一个地址,此时还不是我们要的对象,有意思的是这个size是16字节,一会下面会说到。而当obj->initInstanceIsa(cls, hasCxxDtor);这句走完的时候,obj神奇的就变成了WYPerson对象了,其中initInstanceIsa这个方法就是初始化了isa,并进行了关联。

2.3 一个对象的大小占多少内存空间

曾经面试的时候被问过这个问题,当时答的是16字节,别人问为什么就答不上来了。我们回到_class_createInstanceFromZone方法中的这句代码,的确size就是16

我们去instanceSize方法里面看看

size_t instanceSize(size_t extraBytes) {
        // alignedInstanceSize 对齐当前的实例
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        // 对象的大小一定是8的倍数,当小于8的时候z,直接返回16个字节
        if (size < 16) size = 16;
        return size;
    }

测试创建的WYPerson并没有添加任何东西,这个8其实是isa的大小

alignedInstanceSize,其中word_align内部是会进行位运算,unalignedInstanceSize()代表的是未对齐的内容

uint32_t alignedInstanceSize() {
        // word_align 字节对齐
        return word_align(unalignedInstanceSize()); // 这句话的意思就是没有字节对齐的内容进行字节对齐然后返回
    }

unalignedInstanceSize()

uint32_t unalignedInstanceSize() {
        assert(isRealized());
        // data 是dyld加载的数据段,其实就是当前类的信息
        // ro就是ro_t 编译期确定的一些属性,方法,协议
        return data()->ro->instanceSize;
    }

结论:对于对象所占的内存,会进行内存对齐,不足16的,返回16个字节,这是一种以空间换时间的策略,所以一个对象的大小至少是16字节。

2.4 如何查看内存

2.4.1 lldb

我们在对象内部添加两个个属性

用lldb指令 x 对象,我们这里就是 x p2

但是由于iOS上使用的是小端模式,读的时候得反着读,我们采用另外一个指令 x/4xg 对象,我们这里就是x/4xg p2 ,读取从前往后的多少段内存,这里读取的是4段

2.4.2 Debug - Debug Workflow - View Memory

三 init

其实init才是最简单的,内部实现什么都没有做,仅仅是返回对象,这是工厂设计模式的一种体现,给我们去重写init,做一些初始化工作。

我们常常这么写

- (instancetype)init{
    if (self = [super init]) {
        //一些当前类的初始化操作
    }
    return self;
}

四 alloc流程图