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 道題目。 你們能夠查看並探討一下 , 說一說你的答案 , 若有必要分享一篇解析文章 .

相關文章
相關標籤/搜索