iOS 底層探索 - alloc & init

iOS 底層探索 - alloc & init.png

alloc & init 探索

做爲 iOS 開發者,咱們天天打交道最多的應該就是對象了,從面向對象設計的角度來講,對象的建立以及初始化是最基礎的內容。那麼,今天咱們就一塊兒來探索一下 iOS 中最經常使用的 alloc 和 init  的底層是怎麼實現的吧。html

1、 如何進行底層探索

對於第三方開源框架來講,咱們去剖析內部原理和細節是有必定的方法和套路能夠掌握的。而對於 iOS  底層,特別是 OC 底層,咱們可能就須要用到一些開發中不是很經常使用的方法。程序員

咱們這個系列主要的目的是爲了進行底層探索,那麼咱們做爲 iOS 開發者,須要關注應該就是從應用啓動到應用被 kill 掉這一整個生命週期的內容。咱們不妨從咱們最熟悉的 main 函數開始,通常來講,咱們在 main.m 文件中打一個斷點,左側的調用堆棧視圖應該以下圖所示:macos

image.png

要獲得這樣的調用堆棧有兩個注意點:設計模式

  • 須要關閉 Xcode 左側 Debug 區域最下面的 show only stack frames with debug symbols and between libraries

image.png

  • 須要增長一個 _objc_init 的符號端點

image.png

咱們經過上面的調用堆棧信息不可貴出一個簡單粗略的加載流程結構sass

iOS粗略流程

咱們如今心中創建這麼一個簡單的流程結構,在後期分析底層的時候咱們會回過頭來梳理整個啓動的流程。app

接下來,讓咱們開始實際的探索過程。框架

咱們直接打開 Xcode 新建一個 Single View App 工程,而後咱們在 ViewController.m 文件中調用 alloc 方法。ide

NSObject *p = [NSObject alloc];

咱們按照常規探索源碼的方式,直接按住 Command + Control 來進入到 alloc 內部實現,但結果並不是如咱們所願,咱們來到的是一個頭文件,只有 alloc 方法的聲明,並無對應的實現。這個時候,咱們會陷入深深的懷疑中,其實這個時候咱們只要記住下面三種經常使用探索方式就能迎刃而解:函數

1.1 直接下代碼斷點

具體操做方式爲 Control + in post

image.png 這裏的 in 指的是左側圖片中紅色部分的按鈕,其實這裏的操做叫作 Step into instruction 。咱們能夠來到下圖這裏

image.png

咱們觀察不可貴出咱們想要找的就是 libobjc.A.dylib 這個動態連接庫了。

1.2 打開反彙編顯示

具體操做方式爲打開 Debug 菜單下的 Debug Workflow 下的 Always Show Disassembly 

image.png

接着咱們仍是下代碼斷點,而後一步一步調試也會來到下圖這裏:

image.png

1.3 下符號斷點

咱們先選擇 Symbolic Breakpoint,而後輸入 objc_alloc ,以下圖所示:

image.png image.png

image.png

至此,咱們獲得了 alloc 實現位於 libObjc 這個動態庫,而恰好蘋果已經開源了這部分的代碼,因此咱們能夠在 蘋果開源官網 最新版本 10.14.5 上下載便可。最新的 libObc 爲 756。

image.png

2、 探索 libObjc 源碼

咱們下載了 libObjc 的源碼到咱們的電腦上後是不能直接運行的,咱們須要進行必定的配置才能實現源碼追蹤流程。這一塊內容不在本文範圍內,讀者可參考 iOS_objc4-756.2 最新源碼編譯調試

配置好 libObjc 以後,咱們新建一個命令行的項目,而後運行以下代碼:

NSObject *myObj = [NSObject alloc];

2.1 objc_alloc

而後咱們直接下符號斷點 objc_alloc ,而後一步步調試,先來到的是 objc_alloc 

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

2.2 第一次 callAlloc

而後會來到 callAlloc 方法,注意這裏第三個參數傳的是 false 

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    // 判斷傳入的 checkNil 是否進行判空操做
    if (slowpath(checkNil && !cls)) return nil;

    // 若是當前編譯環境爲 OC 2.0
#if __OBJC2__
    // 當前類沒有自定義的 allocWithZone
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // 既沒有實現 alloc,也沒有實現 allocWithZone 就會來到這裏,下面直接進行內存開闢操做。
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        // 修復沒有元類的類,用人話說就是沒有繼承於 NSObject
        // 判斷當前類是否能夠快速開闢內存,注意,這裏永遠不會被調用,由於 canAllocFast 內部
        // 返回的是false
        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];
}

2.3 _objc_rootAlloc

由於咱們在 objc_init  中傳入的第三個參數 allocWithZone 是 true ,而且咱們的 cls 爲 NSObject ,那麼也就是說會這裏直接來到 return [cls alloc] 。咱們接着往下走會來到 alloc 方法:
 

+ (id)alloc {
    return _objc_rootAlloc(self);
}

而後咱們接着進入 _objc_rootAlloc 方法內部:

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

2.4 第二次 callAlloc

是否是有點似曾類似,沒錯,咱們第一步進入的 objc_init 也是調用的 callAlloc 方法,可是這裏有兩個參數是不同的,第二個參數 checkNil 是否須要判空直接傳的是 false ,站在系統角度,前面已經在第一次調用 callAlloc  的時候進行了判空了,因此這裏不必再次進行判空的了。第三個參數 allocWithZone 傳的是 true ,關於這個方法,我查閱了蘋果開發者文檔,文檔解釋以下:

Do not override allocWithZone: to include any initialization code. Instead, class-specific versions of init... methods.
This method exists for historical reasons; memory zones are no longer used by Objective-C.
譯:不要去重載 allocWithZone 並在其內部填充任何初始化代碼,相反的,應該在 init... 裏面進行類的初始化操做。
這個方法的存在是有歷史緣由的,內存 zone 已經再也不被 Objective-C 所使用的。

按照蘋果開發者文檔的說法,其實 allocWithZone 本質上和 alloc 是沒有區別的,只是在 Objective-C 遠古時代,程序員須要使用諸如 allocWithZone 來優化對象的內存結構,而在當下,其實你寫 alloc 和 allocWithZone 在底層是一模模同樣樣的。

好的,話題扯遠了,咱們接着再次進入到 callAlloc 方法內部,第二次來到 callAlloc 的話,在 !cls->ISA()->hasCustomAWZ() 這裏判斷 cls 沒有自定義的 allocWithZone 實現,這裏的判斷實質上是對 cls 也就是 object_class 這一結構體內部的 class_rw_t 的 flags 與上一個宏 RW_HAS_DEFAULT_AWZ 。通過筆者測試,在第一次進入 callAlloc 方法內部的時候, flags 值爲 1 ,而後  flags 與上 1<<16 結果就是 0 ,返回過去也就是 false ,而後在 hasCustomAWZ 這裏取反以後,返回的就是 true ,而後再一取反,天然就會跳過 if 裏面的邏輯;而第二次進入 callAlloc 方法內部的時候, flags 值是一個很大的整數,與上 1<<16 後結果並不爲0 ,因此 hasDefaultAWZ 會返回 true ,那麼 hasCustomAWZ 這裏就會返回 false ,那麼返回到 callAlloc 的時候天然就會進入 if 裏面的邏輯了。

這裏插一句,在咱們 OC 的類的結構中,有一個結構叫 class_rw_t ,有一個結構叫 class_ro_t 。其中 class_rw_t 是能夠在運行時去拓展類的,包括屬性,方法、協議等等,而 class_ro_t 則存儲了成員變量,屬性和方法等,不過這些是在編譯時就肯定了的,不能在運行時去修改。
bool hasCustomAWZ() {
    return ! bits.hasDefaultAWZ();
 }   

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

而後咱們會來到 canAllocFast 的判斷,咱們繼續進入該方法內部

if (fastpath(cls->canAllocFast()))
bool canAllocFast() {
        assert(!isFuture());
        return bits.canAllocFast();
    }

    bool canAllocFast() {
        return false;
    }

結果很顯然,這裏 canAllocFast 是一直返回 false 的,也就是說會直接來到下面的邏輯

id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;

咱們再次進入 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)
{
    // 對 cls 進行判空操做
    if (!cls) return nil;
    // 斷言 cls 是否實現了
    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    // cls 是否有 C++ 的初始化構造器
    bool hasCxxCtor = cls->hasCxxCtor();
    // cls 是否有 C++ 的析構器
    bool hasCxxDtor = cls->hasCxxDtor();
    // cls 是否能夠分配 Nonpointer,若是是,即表明開啓了內存優化 
    bool fast = cls->canAllocNonpointer();
        
    // 這裏傳入的 extraBytes 爲0,而後獲取 cls 的實例內存大小
    size_t size = cls->instanceSize(extraBytes);
    // 這裏 outAllocatedSize 是默認值 nil,跳過
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    // 這裏 zone 傳入的也是nil,而 fast 拿到的是 true,因此會進入這裏的邏輯
    if (!zone  &&  fast) {
        // 根據 size 開闢內存
        obj = (id)calloc(1, size);
        // 若是開闢失敗,返回 nil
        if (!obj) return nil;
        // 將 cls 和是否有 C++ 析構器傳入給 initInstanceIsa,實例化 isa
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        // 若是 zone 不爲空,通過筆者測試,通常來講調用 alloc 不會來到這裏,只有 allocWithZone
        // 或 copyWithZone 會來到下面的邏輯
        if (zone) {
            // 根據給定的 zone 和 size 開闢內存
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            // 根據 size 開闢內存
            obj = (id)calloc(1, size);
        }
        // 若是開闢失敗,返回 nil
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        // 初始化 isa
        obj->initIsa(cls);
    }

    // 若是有 C++ 初始化構造器和析構器,進行優化加速整個流程
    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    // 返回最終的結果
    return obj;
}

至此,咱們的 alloc 流程就探索完畢,但在這其中咱們仍是有一些疑問點,好比,對象的內存大小時怎麼肯定出來的, isa 是怎麼初始化出來的呢,不要緊,咱們下一篇接着探索。這裏,先給出筆者本身畫的一個 alloc 流程圖,限於筆者水平有限,有錯誤之處望讀者指出:

image.png

2.5 init 簡略分析

分析完了 alloc 的流程,咱們接着分析 init 的流程。相比於 alloc 來講, init 內部實現十分簡單,先來到的是 _objc_rootInit ,而後就直接返回 obj 了。其實這裏是一種抽象工廠設計模式的體現,對於 NSObject 自帶的 init 方法來講,其實啥也沒幹,可是若是你繼承於 NSObject 的話,而後就能夠去重寫 initWithXXX 之類的初始化方法來作一些初始化操做。

- (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;
}

3、總結

先秦荀子的勸學中有言:

不積跬步,無以致千里;不積小流,無以成江海。

咱們在探索 iOS 底層原理的時候,應該也是抱着這樣的學習態度,注意點滴的積累,從小作起,聚沙成塔。下一篇筆者將對本文留下的兩個疑問進行解答:

  • 對象初始化內存是如何分配的?
  • isa 是如何初始化的?
相關文章
相關標籤/搜索