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

前言

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

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

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

这样探索下来 , 咱们不只会熟练掌握这些知识点 , 更能对其融会贯通 , 获得苹果为何会这么设计的根本缘由 . c++

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

资料准备 :swift

OC 对象的建立探索

对象的建立方式 , 最多见的 alloc init , 或者 new .app

新建工程准备代码 :ide

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

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

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

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

一、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 部分有没有彻底开源的代码对此做了相应操做 . 若有知晓 , 欢迎交流 .

二、 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.一、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.二、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.三、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.四、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.一、 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.二、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.三、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.四、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 文章会详细讲述 .

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

三、init

来看下 init

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

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

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

四、new

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

new 至关于 alloc + init . 可是使用 new 并不能调用咱们所重写的各类 init 工厂方法 .

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

分享

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

相关文章
相关标签/搜索