前言
從一個對象的alloc開始,讓我們?nèi)隣C底層實(shí)現(xiàn)陕截,去探索學(xué)習(xí)OC源碼迹恐。
LWPerson * obj = [LWPerson alloc];
LWPerson * obj1 = [obj init];
LWPerson * obj2 = [obj init];
LWPerson * newObj = [LWPerson alloc];
NSLog(@"%@---%p--%p",obj,obj,&obj);
NSLog(@"%@---%p--%p",obj1,obj1,&obj1);
NSLog(@"%@---%p--%p",obj2,obj2,&obj2);
NSLog(@"%@---%p--%p",newObj,newObj,&newObj);
<LWPerson: 0x281404710>---0x281404710--0x16b5cdbe0
<LWPerson: 0x281404710>---0x281404710--0x16b5cdbd8
<LWPerson: 0x281404740>---0x281404740--0x16b5cdbd0
初步總結(jié)
- alloc具有開辟一塊內(nèi)存的功能,而init 沒有開辟內(nèi)存的功能
- ps:棧區(qū) 開辟的內(nèi)存是高地址到低地址塘安,堆區(qū)則是低地址到高地址(這里會延伸出一個奇怪的問題)
三種探索底層的方法
- 符號斷點(diǎn)
- 匯編 Xcode -> Debug -> Debug Workflow -> Always Show Disassembly
- 斷點(diǎn) step into
源碼
alloc
+ (id)alloc {
return _objc_rootAlloc(self);
}
_objc_rootAlloc
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__ //判斷是不是 objc2.0版本
//slowpath(x):x很可能為假糠涛,為真的概率很小
//fastpath(x):x很可能為真
//其實(shí)將fastpath和slowpath去掉是完全不影響任何功能,寫上是告訴編譯器對代碼進(jìn)行優(yōu)化
if (slowpath(checkNil && !cls)) return nil;
//判斷該類是否實(shí)現(xiàn)自自定義的 +allocWithZone,沒有則進(jìn)入if條件句
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
_objc_rootAllocWithZone
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
zone參數(shù)被丟棄兼犯,開發(fā)者即使自定義allocWithZone方法也無法指定想使用的zone忍捡,對象所在zone實(shí)際上由libmalloc庫分配。
對象創(chuàng)建核心函數(shù)_class_createInstanceFromZone
- cls->instanceSize: 對象所需內(nèi)存大小
- (id)calloc(1,size): 開辟內(nèi)存切黔,返回地址指針
- obj->initInstanceIsa: 初始化isa指針砸脊,和類關(guān)聯(lián)起來
- 返回obj(如果有cxxCtor,會調(diào)用初始化)
debug
NSObject *n = [NSObject alloc];
LGPerson *p0 = [LGPerson alloc];
LGPerson *p1 = [LGPerson alloc];
NSLog(@"測試一下");
在creatInstance函數(shù)處打了斷點(diǎn)纬霞,分別打印三次alloc的堆棧
NSObject的alloc
流程:objc_alloc->callAloc->_objc_rootAllocWithZone->createInstance
LGPerson第一次alloc
流程:objc_alloc-> callAloc-> [NSObject alloc]-> _objc_rootAlloc->
callAlloc-> _objc_rootAllocWithZone->createInstance
LGPerson第二次alloc
流程:objc_alloc-> callAlloc-> _objc_rootAllocWithZone-> createInstance
幾個疑問
從函數(shù)椗Ч妫看,[ SomeClass alloc]方法調(diào)用险领,都被轉(zhuǎn)化為objc_alloc函數(shù)調(diào)用侨舆,發(fā)生了什么秒紧,誰做的?
通過查看LLVM源碼挨下,我們發(fā)現(xiàn)是編譯器把a(bǔ)lloc消息發(fā)送熔恢,轉(zhuǎn)化了objc_init函數(shù)調(diào)用。至于為什么臭笆?
我的理解是這里可以看做是編譯器把a(bǔ)lloc方法給hook了叙淌,在創(chuàng)建自定義類模板的第一個實(shí)例對象的時候,objc_alloc會再次發(fā)送alloc消息愁铺,調(diào)用alloc方法鹰霍。其他時候objc_alloc函數(shù)調(diào)用會直接走到createInstane函數(shù)。對比第一次alloc茵乱,這顯然減少后續(xù)對象開辟內(nèi)存空間時的函數(shù)調(diào)用茂洒。
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
為什么自定義類的第一次alloc和第二次alloc的函數(shù)調(diào)用堆棧不同呢?
- debug調(diào)試看到hasCustomAWZ()返回值不同瓶竭。我們第一次alloc多走了一段發(fā)送alloc消息的流程督勺,在這個過程中我們的自定義類被實(shí)現(xiàn),被初始化斤贰,setInitialized()函數(shù)中智哀,cache的_flag成員關(guān)于是有CustomAWZ的標(biāo)記位被重新賦值。
- 除第一次外荧恍,后面alloc流程瓷叫,自定義類如果沒有CustomAWZ,就會直接走_(dá)objc_rootAllocWithZone送巡,不會再經(jīng)過消息發(fā)送赞辩,如果有就會發(fā)送allocWithZone:消息。
- 這也側(cè)面說明授艰,為什么llvm會把a(bǔ)lloc消息發(fā)送轉(zhuǎn)換成objc_alloc,它既保證了我們類在第一次接收消息時被初始化,也避免了頻繁的alloc消息發(fā)送世落,直接進(jìn)去了對象空間開辟流程淮腾。
- 看到這里是不是很期待我們消息發(fā)送流程,以及它是如何觸發(fā)自定義類的初始化屉佳。
為什么NSObject的第一次alloc跟自定義類的不同谷朝,沒有走alloc方法調(diào)用呢?
這里不多說了武花,上圖
- 很顯然NSObject作為基類圆凰,在main函數(shù)之前,程序加載過程中体箕,libobjc就被觸發(fā)了NSObject的initialize方法专钉。這也是我們在main函數(shù)執(zhí)行[NSObject alloc]直接進(jìn)入_objc_rootAllocWithZone的原因挑童。
- 這里需要注意的一點(diǎn)是,NSObjcet類是有自定義awz的,看下面注釋跃须,特殊處理了呀站叼。
// Special cases:
// - NSObject AWZ class methods are default.
- 看到這是不是對我們iOS程序的加載流程也很好奇了。
畫個alloc流程圖吧
截屏2021-07-06 下午6.08.40.png
總結(jié):
- LLVM在編譯時把所有的alloc消息發(fā)送菇民,轉(zhuǎn)化為objc_alloc()
- 自定義類的alloc流程尽楔,如果類沒有initialize,或者實(shí)現(xiàn)了customAWZ(都是customAWZ返回為true)第练,會觸發(fā)alloc消息發(fā)送阔馋,否則將會直接進(jìn)_objc_rootAlloc
- 建議不要customAWZ,因為zone參數(shù)在底層實(shí)現(xiàn)是被丟棄的娇掏,實(shí)際分配zone不會被開發(fā)者決定(libMaolloc分配)呕寝,反而讓alloc調(diào)用流程變復(fù)雜。
- 下篇拓展下Align16驹碍,對齊