alloc & init 探索
作為 iOS
開發(fā)者,我們每天打交道最多的應(yīng)該就是對象了,從面向?qū)ο笤O(shè)計的角度來說,對象的創(chuàng)建以及初始化是最基礎(chǔ)的內(nèi)容。那么,今天我們就一起來探索一下 iOS
中最常用的 alloc
和 init
的底層是怎么實現(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)該如下圖所示:
要得到這樣的調(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)
我們現(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
in
指的是左側(cè)圖片中紅色部分的按鈕,其實這里的操作叫做 Step into instruction
。我們可以來到下圖這里
我們觀察不難得出我們想要找的就是 libobjc.A.dylib
這個動態(tài)鏈接庫了。
1.2 打開反匯編顯示
具體操作方式為打開 Debug
菜單下的 Debug Workflow
下的 Always Show Disassembly
接著我們還是下代碼斷點,然后一步一步調(diào)試也會來到下圖這里:
1.3 下符號斷點
我們先選擇 Symbolic Breakpoint
,然后輸入 objc_alloc
,如下圖所示:
至此,我們得到了 alloc
實現(xiàn)位于 libObjc
這個動態(tài)庫,而剛好蘋果已經(jīng)開源了這部分的代碼,所以我們可以在 蘋果開源官網(wǎng) 最新版本 10.14.5 上下載即可。最新的 libObc
為 756。
二、 探索 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ù) allocWithZone
是 true
,并且我們的 cls
為 NSObject
,那么也就是說會這里直接來到 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 ofinit...
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),而在當下,其實你寫 alloc
和 allocWithZone
在底層是一模模一樣樣的。
好的,話題扯遠了,我們接著再次進入到 callAlloc
方法內(nèi)部,第二次來到 callAlloc
的話,在 !cls->ISA()->hasCustomAWZ()
這里判斷 cls
沒有自定義的 allocWithZone
實現(xiàn),這里的判斷實質(zhì)上是對 cls
也就是 object_class
這一結(jié)構(gòu)體內(nèi)部的 class_rw_t
的 flags
與上一個宏 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
流程圖,限于筆者水平有限,有錯誤之處望讀者指出:
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 是如何初始化的?