阅读 2522

iOS 底层 - OC 对象的创建流程

前言

单独的去了解不同块的知识点并不能帮助我们深入理解和记忆 , 也并不能把知识点串联达到融会贯通 .

从实际场景选择一条主线 , 去了解和学习这条主线所碰到的知识点 , 才能更好的把不同块的只是串联 , 加深理解 .

从对象的创建 , 去探究对象的本质 , 创建的流程 , 一路上会遇到 isa , 对象 -> 类 -> 元类 , cache_t , 内存对齐 , 分类 , taggedPoint , 方法缓存 , 方法查找 , 消息转发 , 内存管理等内容.

这样探索下来 , 我们不仅会熟练掌握这些知识点 , 更能对其融会贯通 , 得到苹果为什么会这么设计的根本原因 .

本篇文章从对象的创建出发 , 梳理对象创建流程 , 探索每一个遇到的知识点 .

资料准备 :

OC 对象的创建探索

对象的创建方式 , 最常见的 alloc init , 或者 new .

新建工程准备代码 :

NSObject * obj = [NSObject alloc];
复制代码

添加好断点 , 运行工程 点击 step into.

我们看到 , 实际调用的是 objc 中的 objc_alloc 函数 . ( 笔者使用的是 Xcode 11 , 使用Xcode 10 会进入 alloc 方法 , 下面会讲解这个问题 ) .

实际在 objc 756.2 运行案例 , 在 allocobjc_alloc 分别添加断点 , 你会发现先走的是 objc_alloc .

1、objc_alloc 与 alloc

但是查阅源码我们看到 NSObject 是有 alloc 类方法的 . 那么我们外部所写的 [NSObject alloc] 为什么不调用 alloc 类方法 , 反而来到了 objc_alloc 中呢 ?

这部分笔者通过一部分源码结合 MachO 文件查看推测如下 :

  • Xcode 10 会直接进入 alloc , Xcode 11 会直接进入 objc_alloc 是因为在 Xcode11 编译后 alloc 对应符号会被设置为 objc_alloc .
  • Xcode 10 并没有 . 我们可以使用 MachOView 分别查看这两种环境下编译的项目 Mach-O , 在 __DATA 段 , __la_symbol_ptr 节中 .

以下为笔者测试结果 .

另外在 objc 源码中查找到部分代码如下 :

static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == SEL_alloc) {
            msg->imp = (IMP)&objc_alloc; // 这里就是符号绑定后对应所做的一些处理了.
        } else if (msg->sel == SEL_allocWithZone) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == SEL_retain) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == SEL_release) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == SEL_autorelease) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
        }
    } 
    /*...*/
}
复制代码

( 关于符号绑定 , 可以阅读一下 Hook / fishHook 原理与符号表从头梳理 dyld 加载流程 这两篇文章 , 本文就不在多阐述了 ) .

alloc 类方法源码如下 :

+ (id)alloc {
    return _objc_rootAlloc(self);
}
复制代码
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
复制代码

objc_alloc 函数如下 :

id objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
复制代码

我们可以看到不管是 alloc 还是 objc_alloc , 都会进入 callAlloc 这个函数 , 只是最后两个参数传入的不同 . 那么我们就继续往下看 .

◈ --> 提示 :

至于在 Xcode 11 调用 [NSObject alloc] 会来到 objc_alloc , 而内部在 callAlloc 函数中 [cls alloc] 则会直接进入 alloc , 笔者还没有查找到确切资料来证实 , 猜测符号绑定和 fixup 部分有没有完全开源的代码对此作了相应操作 . 如有知晓 , 欢迎交流 .

2、 callAlloc

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        if (fastpath(cls->canAllocFast())) {
            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 {
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
复制代码

首先我们注意到了两个宏定义的函数 : fastpathslowpath .

// x 很可能不为 0,希望编译器进行优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能为 0,希望编译器进行优化
#define slowpath(x) (__builtin_expect(bool(x), 0))
复制代码

那么我们就来顺便提一下这个知识点 .

2.1、fastpath 与 slowpath

其实这两个其实将 fastpathslowpath 去掉是完全不影响任何功能的。之所以将 fastpathslowpath 放到 if 语句中,是为了告诉编译器 :

if 中的条件是大概率 ( fastpath ) 还是小概率 ( slowpath ) 事件

从而让编译器对代码进行优化。

那么如何告诉编译器 , 或者说编译器如何针对处理和优化的呢 ?

举个例子 🌰 :

if (x)
    return 2;
else 
    return 1;
复制代码

解读 :

  • 1️⃣ : 由于计算机并非一次只读取一条指令,而是读取多条指令,所以在读到 if 语句时也会把 return 2 读取进来。如果 x0,那么会重新读取 return 1 ,重读指令相对来说比较耗时。

  • 2️⃣ : 如果 x 有非常大的概率是 0,那么return 2 这条指令每次不可避免的会被读取,并且实际上几乎没有机会执行,造成了不必要的指令重读。

  • 3️⃣ : 因此,在苹果定义的两个宏中,fastpath(x) 依然返回 x,只是告诉编译器 x 的值一般不为 0,从而编译可以进行优化。同理,slowpath(x) 表示 x 的值很可能为 0,希望编译器进行优化。

这个例子的讲解来自 bestsswifter深入理解GCD,大家感兴趣可以看看。

因此 我们 callAlloc 中 , 第一步

if (slowpath(checkNil && !cls)) return nil;
复制代码

其实就是告诉编译器 , cls 大概率是有值的 , 编译器对应处理就好 .

那么接下来就来到了 cls->ISA()->hasCustomAWZ() .

2.2、hasCustomAWZ

字面意思看来 , 是判断有没有自己实现 AllocWithZone 方法 . 这个是通过 类的结构体 objc_class 中的 hasCustomAWZ 方法判断的 .

bool hasCustomAWZ() {
    return ! bits.hasDefaultAWZ();
}
复制代码

hasDefaultAWZ 实现如下 :

bool hasDefaultAWZ() {
    return data()->flags & RW_HAS_DEFAULT_AWZ;
}
void setHasDefaultAWZ() {
    data()->setFlags(RW_HAS_DEFAULT_AWZ);
}
void setHasCustomAWZ() {
    data()->clearFlags(RW_HAS_DEFAULT_AWZ);
}
复制代码

其实是在 RW 中所做标记来标识用户有没有自己实现 allocWithZone .

由于类是有懒加载概念的 , 当第一次给该类发消息之前 , 类并没有被加载 , 因此 , 当类第一次接受到 alloc , 进入到 hasCustomAWZ 时 , 并没有 DefaultAWZ , 所以 hasCustomAWZ 则为 true , 因此会直接进入 [cls alloc];

我们可以做一下测试 , 代码如下 :

LBPerson *objc = [[LBPerson alloc] init];
LBPerson *objc1 = [[LBPerson alloc] init];
复制代码

objc 进入到 callAlloc 时 , 会进入下面的 [cls alloc] , 而当 objc1 进入时 , 会直接进入 if (fastpath(!cls->ISA()->hasCustomAWZ())) { 内部 .

提示 :

  • 1️⃣ : 我们所熟知的 initialize , 也是在类接收到第一次消息时 , 在 objc_msgSend 流程被触发调用的 .
  • 2️⃣ : 上述结果为 Xcode 11 环境下 , Xcode 10 环境 直接进入 alloc 即是 objc_msgSend , 因此会直接进入 if 成立流程 .
  • 3️⃣ : 关于 allocWithZone , 我们暂且需要知道的是它是对象开辟的另一种方法 , 如果重写了 , 在 alloc 时 , 则会进入用户自定义的 allocWithZone 流程 . 这也是我们在写单例时 , 也要处理 allocWithZone 的原因 .

lookUpImpOrForward 中针对 initialize 所做处理 .

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();
    /*...*/
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }
    /*...*/
}
复制代码

关于类的结构 以及 isa 的具体内容 , 由于内容较多 , 我会另起两篇文章专门讲述 , 先放一张图 , 方便有个大概理解 .

当第一次进入 [cls alloc]; , 我们来看下源码实现 :

+ (id)alloc {
    return _objc_rootAlloc(self);
}
复制代码
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
复制代码

再次来到 callAlloc 中 , 由于 [cls alloc]; 触发的是消息发送机制 , DefaultAWZtrue , 那么 hasCustomAWZ 则为 false , 因此进入到下个流程 .

2.3、canAllocFast

源码如下 :

bool canAllocFast() {
    assert(!isFuture());
    return bits.canAllocFast();
}
#if !__LP64__
/**/
#elif 1
#else
#define FAST_ALLOC              (1UL<<2)

#if FAST_ALLOC
#else
    bool canAllocFast() {
        return false;
    }
#endif
复制代码

可以很清楚的看到 返回 false . 因此 callAlloc 则来到了

id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
复制代码

至于为什么要这么做 , 其实是因为在 32 位系统下 , 有额外的流程 , 而 64 位系统不再使用 , 因此使用宏定义来处理兼容 .

2.4、class_createInstance

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;
    assert(cls->isRealized());

    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    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);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }
    return obj;
}
复制代码

看函数名称和返回值 , 我们知道来到了重点 , 在这里就开始创建对象 , 分配内存空间了 .

首先是 hasCxxCtorhasCxxDtor . 我们来提一句 .

参考自 :

2.4.1、 hasCxxCtor 与 hasCxxDtor

先来看下 对象的释放流程 .

  • 1️⃣ : 在对象 dealloc 时 , 会判断是否可以被释放,判断的依据主要有 5 个:
    NONPointer_ISA // 是否是非指针类型 isa
    weakly_reference // 是否有若引用
    has_assoc // 是否有关联对象
    has_cxx_dtor // 是否有 c++ 相关内容
    has_sidetable_rc // 是否使用到 sidetable
    复制代码
  • 2️⃣ : 如果没有之前 5 种情况的任意一种,则可以执行释放操作,C 函数的 free() , 执行完毕 , 否则会进入 object_dispose
  • 3️⃣ : object_dispose
    • 直接调用 objc_destructInstance() .
    • 之后调用 C 函数的 free() .
  • 4️⃣ : objc_destructInstance
    • 先判断 hasCxxDtor,如果有 c++ 相关内容,要调用 object_cxxDestruct(),销毁 c++ 相关内容 .
    • 再判断 hasAssociatedObjects,如果有关联对象,要调用 object_remove_associations(),销毁关联对象的一系列操作 .
    • 然后调用 clearDeallocating() .
    • 执行完毕 .
  • 5️⃣ : clearDeallocating() 调用流程
    • 先执行 sideTable_clearDeallocating() .
    • 再执行 waek_clear_no_lock,将指向该对象的弱引用指针置为 nil .
    • 接下来执行 table.refcnts.eraser(),从引用计数表中擦除该对象的引用计数 .
    • 至此为此,dealloc 的执行流程结束 .

这两个其实一开始是 objc++ 中用来处理 c++ 成员变量的构造和析构的,后来 .cxx_destruct 也用来处理 ARC 下的内存释放。

  • 在使用 MRC 时,开发人员必须手动编写 dealloc 以确保释放对其保留的所有对象的所有引用。这是手动操作,容易出错。

  • 引入 ARC 时,执行与这些手动发行版等效的任务的代码必须在每个具有除简单属性之外的所有对象的对象中实现。依靠开发人员手动实现 dealloc 例程将无法解决这一问题。

  • 因此使用了 objective-c ++ 的预先存在的机制,即一个被称为隐藏选择器,该选择器 ( .cxx_destruct ) 在对象被释放之前自动被 Objective C 运行时调用 , 它们由编译器自动生成 。

因此 hasCxxCtorhasCxxDtor , 就是为了标记是否有这两个选择器 .

可能有同学注意过 , 在我们获取类的方法列表时就有 .cxx_destruct .

测试 :

void testObjc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        NSLog(@"Method, name: %@", key);
    }
    free(methods);
}
复制代码

打印如下 :

因此 , .cxx_destruct 也被常称为 隐藏选择器 .

回到 class_createInstance 中来 , 下一步 , canAllocNonpointer , 这里在 isa 中会详细讲述 . 接下来来到 size_t size = cls->instanceSize(extraBytes);

2.4.2、instanceSize

到这里就开始计算所需开辟内存空间了 , 也就涉及到了经常被提及的 内存对齐 .

关于开辟内存 , OC对象占用内存原理 这篇文章中也有详细讲述 .

先来看源码 :

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF 要求 all objects 需要最少为 16 bytes.
    if (size < 16) size = 16;
    return size;
}

// Class's ivar size 四舍五入 to a pointer-size boundary.
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}
复制代码
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif
复制代码

instanceSize 传入参数 extraBytes0 , 从上面源码我们首先可以看到 , 属性64 位下满足 8 字节对齐 , 32 位下满足 4 字节对齐 .

使用的是 (x + WORD_MASK) & ~WORD_MASK ; . 跟位运算左移三位右移三位是同样的效果 , 类结构体 RO 中的信息在编译期就已经确定了 ( data()->ro->instanceSize , 也就是 unalignedInstanceSize ) .

同时 , 满足最小 16 字节 ( if (size < 16) size = 16 ) .

那么接下来 , 由于传入 zoneNULL , 并且是支持 Nonpointer isa 的 . 因此来到 if 满足语句中 .

id obj;
if (!zone  &&  fast) {
    obj = (id)calloc(1, size);
    if (!obj) return nil;
    obj->initInstanceIsa(cls, hasCxxDtor);
} 
复制代码

2.4.3、calloc

点击进去发现 calloc 源码在 malloc 中 .

void * calloc(size_t num_items, size_t size)
{
    void *retval;
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
	errno = ENOMEM;
    }
    return retval;
}
复制代码

小提示 : 在跟不进源码时可以按照以下方式

最后跟到这里 :

static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
	MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

	void *ptr;
	size_t slot_key;
	size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
	mag_index_t mag_index = nano_mag_index(nanozone);

	nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

	ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
	if (ptr) {
	    /**省略*/
	} else {
	    ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
	}

	if (cleared_requested && ptr) {
	    memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
	}
	return ptr;
}
复制代码

其中 segregated_size_to_fit 如下 :

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;// multiply by power of two quanta size
    *pKey = k - 1;// Zero-based!

    return slot_bytes;
}

#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	// 16
复制代码

可以看出 slot_bytes 相当于 (size + (16-1) ) >> 4 << 4,也就是 16 字节对齐,因此 calloc() 分配的对象内存是按 16 字节对齐标准的 .

那么 calloc 开辟了内存空间 , 并返回一个指向该内存地址的指针 . 回到 libobjc , _class_createInstanceFromZone 接下来 .

obj->initInstanceIsa(cls, hasCxxDtor);
复制代码

2.4.4、initInstanceIsa

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    assert(!cls->instancesRequireRawIsa());
    assert(hasCxxDtor == cls->hasCxxDtor());
    initIsa(cls, true, hasCxxDtor);
}
复制代码
inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        /*arm64 不走这里*/
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}
复制代码

这里就是初始化 isa , 并绑定指向 cls : newisa.shiftcls = (uintptr_t)cls >> 3; 后续 isa 文章会详细讲述 .

至此 , 对象的创建已经探索完毕了 . 释放过程我们也稍微讲述了一下 .

3、init

来看下 init

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

id _objc_rootInit(id obj)
{
    return obj;
}
复制代码

可以看到 init 默认返回方法调用者 . 这个设计其实是为了方便工程设计 , 以便于在初始化对象时做一些初始化或者赋值操作 .

4、new

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

new 相当于 alloc + init . 但是使用 new 并不能调用我们所重写的各种 init 工厂方法 .

有小道消息说是为了 java 等语言的开发者的习惯问题加入的 , 听一听就得了 , 当不得真 .

分享

最后分享一下 , sunnyxx 在线下的一次分享会上给了 4 道题目。 大家可以查看并探讨一下 , 说一说你的答案 , 如有必要分享一篇解析文章 .

文章分类
iOS
文章标签