iOS探索 -- alloc、init 与 new 的分析

1,525 阅读5分钟

iOS 探索系列相关文章 :

iOS 探索 -- alloc、init 与 new 的分析

iOS 探索 -- 内存对齐原理分析

iOS 探索 -- isa 的初始化和指向分析

iOS 探索 -- 类的结构分析(一)

iOS 探索 -- 类的结构分析(二)

iOS 探索 -- 消息查找流程(一)

iOS 探索 -- 消息查找流程(二)

iOS 探索 -- 动态方法决议分析

iOS 探索 -- 消息转发流程分析

iOS 探索 -- 离屏渲染

iOS 探索 -- KVC 原理分析

一. 源码跟踪

一般情况下, 当我们想要去了解某个方法的实现的时候, 我们可以在工程里面 command + Ctrl 然后点击我们想要查看的方法。但是在系统方法上却不能找到我们想要的答案, 因为苹果公司并没有把所有方法的实现开源出来, 下面介绍几种寻找源码实现的方法:

方法断点:

第一种方法就是通过给当前方法下断点, 然后逐步往后运行的方法。需要注意的地方如下图:

图 1

需要按住 Ctrl 然后一步步往下执行, 才能找到我们需要的东西。还有就是需要在真机上进行调试, 模拟器会在 'pushq' 和 'jmp' 之间一直循环。

图 2

最后得到的结果如上图2, libobjc.A.dylib 就是我们所要找的东西。

符号断点:

通过在程序运行中加入 符号断点 来拦截当前正在执行的方法, 如下图:

图 3

不过需要注意的是, 在开始打符号断点之前, 首先要先等程序走到我们之前打过的方法断点。因为比如上图的 alloc 方法, 系统有太多的类会去调用该方法, 如果不先定位到方法断点, 就会一直走到符号断点, 无法确认当前 alloc 方法到底是哪个类调用的。

汇编:

使用这个方法首先需要设置 xcode->Debug->Debug workflow->勾选 always Show Disassembly , 然后重新启动程序。 在程序运行到我们的断点的时候就会直接进入汇编界面, 如下图:

图 4

找到我们想要了解的方法, 上图中红框内的 objc_alloc, 就是我想要去找的方法。然后继续断点, 按住 Ctrl , 一步步执行, 一直到如下页面:

图 5

最后的结果如上图

二. alloc & init & new 探究

准备:

通过上面的方法, 我们找到了我们的方法所在的位置。然后下一步就去展开对源码的探究吧, 具体准备如下:

objc4-750源码 + Xcode 11 + MacOS 10.15

官方源码地址

官方的源码下载下来是不能直接编译的, 还需要做一下后面的处理, 具体处理步骤在这里:iOS_objc4-756.2 最新源码编译调试 , 当然你也可以直接下载下来。

1. alloc 原理:

环境配置好之后, 在 alloc 方法打上断点, 然后一步步往下层查看。

图6 准备代码
alloc 流程图:

废话少说, 先放上我根据调试流程画的 alloc 流程图:



图7 alloc 流程图

从流程图上可以看出, 执行顺序为 alloc -> objc_rootAlloc -> callAlloc , 在 callAlloc() 方法执行以后, 源码出现了一些判断(这里只保留了主要部分代码), 程序开始出现分叉, 下面是方法源码:

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    #if __OBJC2__
    
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
    
    // No alloc/allocWithZone implementation. Go straight to the allocator.
    // 没有alloc/allocWithZone实现. 直接去找 allocator.
    
    // fixme store hasCustomAWZ in the non-meta class and 
    // add it to canAllocFast's summary
    // fixme store 非元类中的 hasCustomAWZ 并且 将其添加到 canAllocFast 的摘要里面去
    
        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];
}

接下来来看一下if语句中的判断条件:

// hasCustomAWZ()的实现

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

bool hasDefaultAWZ() {
    return data()->flags & RW_HAS_DEFAULT_AWZ;
}

// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ    (1<<16)

/................分割线..................../

// canAllocFast()的实现

bool canAllocFast() {
    assert(!isFuture());
    return bits.canAllocFast();
}

// bits.canAllocFast()
#if FAST_ALLOC
    ......
#else
    size_t fastInstanceSize() {
        abort();
    }
    void setFastInstanceSize(size_t) {
        // nothing
    }
    bool canAllocFast() {
        return false;
    }

// FAST_ALLOC 的声明
#if !__LP64__
    ......
#elif 1
    ......
#else
    // summary bit for fast alloc path: !hasCxxCtor and 
    //   !instancesRequireRawIsa and instanceSize fits into shiftedSize
    #define FAST_ALLOC              (1UL<<2)

#endif

关于最外层的判断条件 hasCustomAWZ 可以根据其名字进行大概猜测, 就是 有没有自定义的 allocWithZone: 方法, 如果有的话就跳出判断直接走到 return 的地方. 关于这个判断涉及的知识我目前还不太确定, 只是感觉应该跟 isa 的存储信息有关, 有兴趣的可以继续去了解一下。

对于 canAllocFast() 的判断, 关键点在于 FAST_ALLOC 的宏定义声明, 可以看到在 64位 下会走到 #else 1 里面去, 但是里面根本就没有定义过 FAST_ALLOC, 所以canAllocFast()一直都会是 false。

接下来来看一下 id obj = class_createInstance(cls, 0); 方法, 从方法名字就可以得出 "创建对象":

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

static __attribute__((always_inline)) 
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil) {
    if (!cls) return nil;
    // Locking: To prevent concurrent realization, hold runtimeLock.
    // 加锁, 防止该步骤的并发实现, 保持运行时锁定
    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer(); // 是否需要初始化 isa 指针

    // 从传参可以得出此时的 extraBytes 为 0
    // instanceSize 方法返回一个 size,也就是对象实际需要的 size 大小
    size_t size = cls->instanceSize(extraBytes); 
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size); // 内存申请
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor); // 初始化 isa
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }
    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }
    return obj;
}

通过上面代码的相关调用方法名, 我们可以大概猜到 内存申请 (id)calloc(1, size); 初始化 isa指针 obj->initInstanceIsa(cls, hasCxxDtor); 的方法 (到这里想到一句话不知道对不对,对象的创建过程就是 isa 的初始化和对象内存开辟的过程), 在这前面还有一个方法,就是对对象开辟内存大小计算的方法:

size_t size = cls->instanceSize(extraBytes); // 获取需要开辟的内存大小

size_t instanceSize(size_t 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() {
  	return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
    // 7+8 = 15
    // 0000 1111
    // 0000 1000
    //&
    // 1111 1000 ~7
    // 0000 1000 8
    
    // 0000 0111
    //
    // x + 7
    // 8
    // 8 二阶
    // (x + 7) >> 3 << 3
    return (x + WORD_MASK) & ~WORD_MASK;
}

这个方法中涉及到两个内容,首先是通过 word_align 方法对属性进行的 8字节 对齐,然后又通过 if(size < 16)size = 16; 对对象大小进行了 16 字节对齐,关于这一块的内容可以看一下这里的内容 iOS 探索-- 内存对齐原理分析 - 掘金 (juejin.cn)

2. 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 方法就相对简单了, 因为他什么都没有做, 只是 返回了 self。init 方法之所以这样实现其实是为了提供 工厂设计模式 下的接口方法, 给子类去自定义重写该方法。

谈到 init , 我们会想起在日常写代码中用到的写法 self = [super init] , 那么我们为什么要这样写呢 ? 结合自己的想法 和 网上找到的意见我总结出一下几点:

  1. 想要确定父类在初始化中是否失败或者直接返回 nil
  2. 父类有可能不是去返回self, 而是去返回另外一个不同的对象。这种情况我们可能很少见, 在苹果的 Foundation 框架中存在一种设计模式 类集群(类簇) , 该模式下就存在这种情况。(扩展: 有关类簇的探讨)
  3. 单例对象, 假如父类是单例的话这里的返回同样会出问题

3. New 方法:

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

通过查看 new 方法的源码, 我们发现该方法总共做了两件事。首先通过 callAlloc 方法申请内存, 然后再去调用 init 方法, 其实就是对上面两个方法的整合。