iOS 底層探索 - alloc & init

iOS 底層探索 - alloc & init.png

alloc & init 探索

作為 iOS 開發(fā)者,我們每天打交道最多的應(yīng)該就是對象了,從面向?qū)ο笤O(shè)計的角度來說,對象的創(chuàng)建以及初始化是最基礎(chǔ)的內(nèi)容。那么,今天我們就一起來探索一下 iOS 中最常用的 allocinit 的底層是怎么實現(xiàn)的吧。

一、 如何進行底層探索

對于第三方開源框架來說,我們?nèi)テ饰鰞?nèi)部原理和細節(jié)是有一定的方法和套路可以掌握的。而對于 iOS 底層,特別是 OC 底層,我們可能就需要用到一些開發(fā)中不是很常用的方法。

我們這個系列主要的目的是為了進行底層探索,那么我們作為 iOS 開發(fā)者,需要關(guān)注應(yīng)該就是從應(yīng)用啟動到應(yīng)用被 kill 掉這一整個生命周期的內(nèi)容。我們不妨從我們最熟悉的 main 函數(shù)開始,一般來說,我們在 main.m 文件中打一個斷點,左側(cè)的調(diào)用堆棧視圖應(yīng)該如下圖所示:

image.png

要得到這樣的調(diào)用堆棧有兩個注意點:

  • 需要關(guān)閉 Xcode 左側(cè) Debug 區(qū)域最下面的 show only stack frames with debug symbols and between libraries

[圖片上傳中...(image-7ff380-1577068503242-1)]

  • 需要增加一個 _objc_init 的符號端點
image.png

我們通過上面的調(diào)用堆棧信息不難得出一個簡單粗略的加載流程結(jié)構(gòu)

iOS粗略流程

我們現(xiàn)在心中建立這么一個簡單的流程結(jié)構(gòu),在后期分析底層的時候我們會回過頭來梳理整個啟動的流程。

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

我們直接打開 Xcode 新建一個 Single View App 工程,然后我們在 ViewController.m 文件中調(diào)用 alloc 方法。

NSObject *p = [NSObject alloc];

我們按照常規(guī)探索源碼的方式,直接按住 Command + Control 來進入到 alloc 內(nèi)部實現(xiàn),但結(jié)果并非如我們所愿,我們來到的是一個頭文件,只有 alloc 方法的聲明,并沒有對應(yīng)的實現(xiàn)。這個時候,我們會陷入深深的懷疑中,其實這個時候我們只要記住下面三種常用探索方式就能迎刃而解:

1.1 直接下代碼斷點

具體操作方式為 Control + in

image.png
這里的 in 指的是左側(cè)圖片中紅色部分的按鈕,其實這里的操作叫做 Step into instruction 。我們可以來到下圖這里

image.png

我們觀察不難得出我們想要找的就是 libobjc.A.dylib 這個動態(tài)鏈接庫了。

1.2 打開反匯編顯示

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

image.png

接著我們還是下代碼斷點,然后一步一步調(diào)試也會來到下圖這里:

image.png

1.3 下符號斷點

我們先選擇 Symbolic Breakpoint,然后輸入 objc_alloc ,如下圖所示:

image.png

image.png

至此,我們得到了 alloc 實現(xiàn)位于 libObjc 這個動態(tài)庫,而剛好蘋果已經(jīng)開源了這部分的代碼,所以我們可以在 蘋果開源官網(wǎng) 最新版本 10.14.5 上下載即可。最新的 libObc 為 756。

image.png

二、 探索 libObjc 源碼

我們下載了 libObjc 的源碼到我們的電腦上后是不能直接運行的,我們需要進行一定的配置才能實現(xiàn)源碼追蹤流程。這一塊內(nèi)容不在本文范圍內(nèi),讀者可參考 iOS_objc4-756.2 最新源碼編譯調(diào)試

配置好 libObjc 之后,我們新建一個命令行的項目,然后運行如下代碼:

NSObject *myObj = [NSObject alloc];

2.1 objc_alloc

然后我們直接下符號斷點 objc_alloc ,然后一步步調(diào)試,先來到的是 objc_alloc

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

2.2 第一次 callAlloc

然后會來到 callAlloc 方法,注意這里第三個參數(shù)傳的是 false

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

    // 如果當前編譯環(huán)境為 OC 2.0
#if __OBJC2__
    // 當前類沒有自定義的 allocWithZone
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // 既沒有實現(xiàn) alloc,也沒有實現(xiàn) allocWithZone 就會來到這里,下面直接進行內(nèi)存開辟操作。
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        // 修復(fù)沒有元類的類,用人話說就是沒有繼承于 NSObject
        // 判斷當前類是否可以快速開辟內(nèi)存,注意,這里永遠不會被調(diào)用,因為 canAllocFast 內(nèi)部
        // 返回的是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 中傳入的第三個參數(shù) allocWithZonetrue ,并且我們的 clsNSObject ,那么也就是說會這里直接來到 return [cls alloc] 。我們接著往下走會來到 alloc 方法:

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

然后我們接著進入 _objc_rootAlloc 方法內(nèi)部:

// 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 也是調(diào)用的 callAlloc 方法,但是這里有兩個參數(shù)是不一樣的,第二個參數(shù) checkNil 是否需要判空直接傳的是 false ,站在系統(tǒng)角度,前面已經(jīng)在第一次調(diào)用 callAlloc 的時候進行了判空了,所以這里沒必要再次進行判空的了。第三個參數(shù) allocWithZone 傳的是 true ,關(guān)于這個方法,我查閱了蘋果開發(fā)者文檔,文檔解釋如下:

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 并在其內(nèi)部填充任何初始化代碼,相反的,應(yīng)該在 init... 里面進行類的初始化操作。

這個方法的存在是有歷史原因的,內(nèi)存 zone 已經(jīng)不再被 Objective-C 所使用的。

按照蘋果開發(fā)者文檔的說法,其實 allocWithZone 本質(zhì)上和 alloc 是沒有區(qū)別的,只是在 Objective-C 遠古時代,程序員需要使用諸如 allocWithZone 來優(yōu)化對象的內(nèi)存結(jié)構(gòu),而在當下,其實你寫 allocallocWithZone 在底層是一模模一樣樣的。

好的,話題扯遠了,我們接著再次進入到 callAlloc 方法內(nèi)部,第二次來到 callAlloc 的話,在 !cls->ISA()->hasCustomAWZ() 這里判斷 cls 沒有自定義的 allocWithZone 實現(xiàn),這里的判斷實質(zhì)上是對 cls 也就是 object_class 這一結(jié)構(gòu)體內(nèi)部的 class_rw_tflags 與上一個宏 RW_HAS_DEFAULT_AWZ 。經(jīng)過筆者測試,在第一次進入 callAlloc 方法內(nèi)部的時候, flags 值為 1 ,然后 flags 與上 1<<16 結(jié)果就是 0 ,返回過去也就是 false ,然后在 hasCustomAWZ 這里取反之后,返回的就是 true ,然后再一取反,自然就會跳過 if 里面的邏輯;而第二次進入 callAlloc 方法內(nèi)部的時候, flags 值是一個很大的整數(shù),與上 1<<16 后結(jié)果并不為0 ,所以 hasDefaultAWZ 會返回 true ,那么 hasCustomAWZ 這里就會返回 false ,那么返回到 callAlloc 的時候自然就會進入 if 里面的邏輯了。

這里插一句,在我們 OC 的類的結(jié)構(gòu)中,有一個結(jié)構(gòu)叫 class_rw_t ,有一個結(jié)構(gòu)叫 class_ro_t 。其中 class_rw_t 是可以在運行時去拓展類的,包括屬性,方法、協(xié)議等等,而 class_ro_t 則存儲了成員變量,屬性和方法等,不過這些是在編譯時就確定了的,不能在運行時去修改。

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

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

然后我們會來到 canAllocFast 的判斷,我們繼續(xù)進入該方法內(nèi)部

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

bool canAllocFast() {
    return false;
}       

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

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

我們再次進入 class_createInstance 方法內(nèi)部

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 是否實現(xiàn)了
    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    // cls 是否有 C++ 的初始化構(gòu)造器
    bool hasCxxCtor = cls->hasCxxCtor();
    // cls 是否有 C++ 的析構(gòu)器
    bool hasCxxDtor = cls->hasCxxDtor();
    // cls 是否可以分配 Nonpointer,如果是,即代表開啟了內(nèi)存優(yōu)化 
    bool fast = cls->canAllocNonpointer();
        
    // 這里傳入的 extraBytes 為0,然后獲取 cls 的實例內(nèi)存大小
    size_t size = cls->instanceSize(extraBytes);
    // 這里 outAllocatedSize 是默認值 nil,跳過
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    // 這里 zone 傳入的也是nil,而 fast 拿到的是 true,所以會進入這里的邏輯
    if (!zone  &&  fast) {
        // 根據(jù) size 開辟內(nèi)存
        obj = (id)calloc(1, size);
        // 如果開辟失敗,返回 nil
        if (!obj) return nil;
        // 將 cls 和是否有 C++ 析構(gòu)器傳入給 initInstanceIsa,實例化 isa
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        // 如果 zone 不為空,經(jīng)過筆者測試,一般來說調(diào)用 alloc 不會來到這里,只有 allocWithZone
        // 或 copyWithZone 會來到下面的邏輯
        if (zone) {
            // 根據(jù)給定的 zone 和 size 開辟內(nèi)存
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            // 根據(jù) size 開辟內(nèi)存
            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++ 初始化構(gòu)造器和析構(gòu)器,進行優(yōu)化加速整個流程
    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    // 返回最終的結(jié)果
    return obj;
}

至此,我們的 alloc 流程就探索完畢,但在這其中我們還是有一些疑問點,比如,對象的內(nèi)存大小時怎么確定出來的, isa 是怎么初始化出來的呢,沒關(guān)系,我們下一篇接著探索。這里,先給出筆者自己畫的一個 alloc 流程圖,限于筆者水平有限,有錯誤之處望讀者指出:

image.png

2.5 init 簡略分析

分析完了 alloc 的流程,我們接著分析 init 的流程。相比于 alloc 來說, init 內(nèi)部實現(xiàn)十分簡單,先來到的是 _objc_rootInit ,然后就直接返回 obj 了。其實這里是一種抽象工廠設(shè)計模式的體現(xiàn),對于 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;
}

三、總結(jié)

先秦荀子的勸學中有言:

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

我們在探索 iOS 底層原理的時候,應(yīng)該也是抱著這樣的學習態(tài)度,注意點滴的積累,從小做起,積少成多。下一篇筆者將對本文留下的兩個疑問進行解答:

  • 對象初始化內(nèi)存是如何分配的?
  • isa 是如何初始化的?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內(nèi)容