一载荔、探索歷程
思考:從哪里開始探索? -> 對象的初始化盾饮?-> [對象 alloc]?
不管三七二十一,既然是探索alloc流程,那就先整一個alloc來玩一玩
-
創(chuàng)建一個GomuPerson對象
- 初始化對象:通過下面一個小小的操作來引入思考
GomuPerson *p1 = [GomuPerson alloc];
GomuPerson *p2 = [p1 init];
GomuPerson *p3 = [p1 init];
//: 問題: p1,p2,p3 是相同的對象丘损,還是不同的對象普办?
//: 猜是沒有任何結(jié)果的,那我們不妨打印一下徘钥,讓結(jié)果更直觀的出現(xiàn)在我們面前衔蹲,走你~
NSLog(@"p1 : %@",p1);
NSLog(@"p2 : %@",p2);
NSLog(@"p3 : %@",p3);
//: log 如下:
//: p1 : <GomuPerson: 0x600000376a40>
//: p2 : <GomuPerson: 0x600000376a40>
//: p3 : <GomuPerson: 0x600000376a40>
結(jié)論:
p1,p2,p3 對象的內(nèi)存指針地址相同,既然他們的內(nèi)存指針地址相同呈础,那我們可以推斷他們是相同的對象舆驶。
思考:那指向內(nèi)存指針地址的指針地址是否相同呢?
那我們就打印一下指向?qū)ο笾羔樀刂返闹羔樀刂?/p>
NSLog(@"p1 : %p",&p1);
NSLog(@"p2 : %p",&p2);
NSLog(@"p3 : %p",&p3);
//: log 如下:
//: p1 : 0x7ffee62df138
//: p2 : 0x7ffee62df130
//: p3 : 0x7ffee62df128
結(jié)論:
- 指向內(nèi)存指針指針的指針地址不相同而钞。不同的指針指向了共用同一塊內(nèi)存指針地址的對象
- 由p1,p2,p3的指針地址推斷沙廉,棧內(nèi)存是連續(xù)的(0x30 + 0x08 = 0x38, 0x28 + 0x08 = 0x30 [這里是16進制運算]),并且所占內(nèi)存都是8字節(jié)(指針占內(nèi)存8字節(jié))
- init不會對我們開辟的對象的內(nèi)存空間進行修改臼节,地址指針的創(chuàng)建來自于alloc
附圖
二撬陵、開始探索
首先想到就是 command + 鼠標左鍵點擊 alloc 進入源碼,但是失敗了网缝,發(fā)現(xiàn)這部分代碼沒有開源巨税,那我們應(yīng)該該怎么辦呢?
蘋果開源庫:
1:https://opensource.apple.com/source 所以開源庫都在里面,包括各個老版本
2:https://opensource.apple.com 可以根據(jù)系統(tǒng)版本選擇更新的下載
知道哪里下載庫了粉臊,那我們怎么知道alloc屬于哪個庫呢草添?
這里為大家提供3個方法,供參考
方法一: 下符號斷點的形式直接跟流程
-
下符號斷點的方法
-
先在我們要研究的對象GomuPerson初始化處下一個斷點
- 執(zhí)行程序到斷點處
-
因為我們要研究alloc扼仲,所以下一個名為alloc的符號斷點
-
讓程序繼續(xù)走
- 得到我們想要的東西了
libobjc.A.dylib
远寸,推測alloc 在這個庫中
- 這里為什么走到了
[NSObject alloc]
方法,而不是[GomuPerson alloc]屠凶?
- 因為GomuPerson 繼承 NSObject而晒,而且 GomuPerson 里面沒有 alloc方法
方法二:Ctrl + step into
-
首先在我們還是要研究的對象GomuPerson初始化處下一個斷點
-
讓程序執(zhí)行到這里之后,按住Ctrl阅畴,這個下一步的按鈕就會變成如下圖所示
- 按住Ctrl,多點幾次下一步迅耘,來到了
objc_alloc
- 把
objc_alloc
作為符號斷點贱枣,加上的一瞬間就又找到了libobjc.A.dylib
方法三:匯編查看跟流程(最常用的)
-
首先在我們還是要研究的對象GomuPerson初始化處下一個斷點
-
Debug -> Debug Workflow -> Always Show Disassembly 進入?yún)R編
- 得到
objc_alloc
- 用方法二下符號斷點又可以拿到
libobjc.A.dylib
三、找到源碼庫libobjc.A.dylib
即 objc4
下載源碼+編譯源碼請移步到 iOS_objc4-781.2 最新源碼編譯調(diào)試
四颤专、打開源碼工程纽哥,開始alloc探索之旅
方法一: 通過 command + 鼠標左鍵 一步一步進入源碼看流程
- 進入
alloc
- 進入
[NSObject alloc]
- 進入
_objc_rootAlloc
- 進入
callAlloc
由上面步驟我們可以先梳理一個流程如下:
那么問題來了:
問題一:進入callAlloc
方法之后,是進入objc_msgSend
呢栖秕,還是進入_objc_rootAllocWithZone
?
問題二:我們根據(jù)代碼走查梳理出來的步驟是否準確春塌?
方法二:下符號斷點,驗證上面問題和流程
-
我們下了如下3個符號斷點
-
打開Debug -> Debug Workflow -> Always Show Disassembly 進入?yún)R編然后打開上面3個斷點,通過斷點調(diào)試只壳,得到以下流程
結(jié)論:
- 我們剛剛走查代碼梳理的流程錯誤俏拱?因為缺少了
callAlloc
這步 - 執(zhí)行到
callAlloc
方法之后,下一步執(zhí)行_objc_rootAllocWithZone
吼句,沒有執(zhí)行objc_msgSend
方法三:進入源碼下斷點锅必,驗證上面結(jié)論1和2。
問題:由于objc_alloc
和_objc_rootAlloc
都是調(diào)用的callAlloc
惕艳,那他們到底是怎樣調(diào)用的搞隐?是兩個都要調(diào)用嗎?那callAlloc豈不是要走2次远搪?
-
下四個斷點如下圖
- 經(jīng)過觀察發(fā)現(xiàn)劣纲,
objc_alloc
和_objc_rootAlloc
的第二個入?yún)?code>checkNil不同
- 經(jīng)過一番斷點調(diào)試得出
objc_alloc
和_objc_rootAlloc
都會調(diào)用,callAlloc
確實要走兩次谁鳍,第一次調(diào)用objc_alloc
時癞季,callAlloc
方法會走最后一句return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))
,接著調(diào)用_objc_rootAlloc
方法時棠耕,callAlloc
方法才會調(diào)用_objc_rootAllocWithZone
方法余佛,得出以下流程圖
那么問題又來了:callAlloc
被調(diào)用了兩次,為什么匯編調(diào)試的時候窍荧,一次都沒有走符號斷點呢辉巡?
這個問題我們下次單獨一期來講解,它牽涉到<編譯優(yōu)化>蕊退,有興趣的朋友可以先自行研究郊楣。
繼續(xù)探索
_objc_rootAllocWithZone
之后的流程
- 調(diào)用alloc的核心方法
_class_createInstanceFromZone
- 經(jīng)過一系列斷點調(diào)試,發(fā)現(xiàn)
_objc_rootAllocWithZone
會走這三步瓤荔,那我可以開始思考净蚤?alloc
既然是開辟內(nèi)存,那如果讓我們開辟內(nèi)存输硝,我們會怎么做呢谭梗?1.計算內(nèi)存大小。2.向系統(tǒng)申請內(nèi)存熊锭。3.內(nèi)存與類關(guān)聯(lián)幌衣。從上圖方法名我們也不難猜測出這3步的作用。得到以下的流程圖:
五郎逃、拓展知識
開辟空間是怎么進行16位內(nèi)存對齊的(為什么16位哥童?哈,蘋果爸爸規(guī)定的褒翰,以前是8位)
通過一個很6的算法:(x + size_t(15)) & ~size_t(15)
//: 比如當前x 傳入8字節(jié)
8 + 15 = 23
換算成16位贮懈,2進制:
0000 0000 0001 0111
//: ~size_t(15) 先把15換算成2進制
0000 0000 0000 1111
//: 取反 并與23 & 運算
1111 1111 1111 0000
0000 0000 0001 0111
0000 0000 0001 0000
//: 結(jié)果為16匀泊,從這個運算不難看出,15取反后后四位都是0朵你,這個算法就是抹掉后四位各聘,那就從倒數(shù)第五位開始運算,則結(jié)果都為16倍數(shù)
init
new
做了什么事撬呢?
- init
//: init源碼
- (id)init {
return _objc_rootInit(self);
}
_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;
}
//: 從上面源碼不難看出伦吠,init什么也沒做,就返回了自己
結(jié)論:init的作用:構(gòu)造方法魂拦,也是一種工廠設(shè)計毛仪,方便開發(fā)者重新,給開發(fā)者提供相應(yīng)入口芯勘,比如:initWithFrame
- new
//: new源碼
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
//: [GomuPerson new] 就相當于 [[GomuPerson alloc] init]
結(jié)論:經(jīng)過一番斷點調(diào)試箱靴,發(fā)現(xiàn)[GomuPerson new]和第一次GomuPerson *p = [GomuPerson alloc]
的流程一模一樣,所以 new:相當于(alloc + init)
GomuPerson *p = [GomuPerson alloc];
GomuPerson *p1 = [GomuPerson alloc];
再初始化一個對象p1荷愕,會發(fā)現(xiàn)p1的流程又變了衡怀,如圖:
結(jié)論:第二次初始化相同對象的時候,不會再走_objc_rootAlloc
這個流程安疗。
那為什么呢抛杨?
NSObject *obj = [NSObject alloc];
和上面初始化p1的流程一樣
問題:為什么p1和obj都不走_objc_rootAlloc
呢?
- 先回答第一個問題:為什么
[NSObject alloc]
怖现,不走_objc_rootAlloc
方法:
-
在main函數(shù)之前打個斷點
- 添加一個
alloc
的符號斷點
結(jié)論:原來進入main
函數(shù)之前,_objc_rootAlloc
函數(shù)已經(jīng)被調(diào)用了玉罐,就相當于初始化p的流程屈嗤,所以后面我們再次初始化[NSObject alloc]
的時候,就和初始化p1的流程一樣了吊输。
- 然后我們?nèi)∠?code>main函數(shù)的斷點
- 添加一個
objc_alloc
的符號斷點
發(fā)現(xiàn)系統(tǒng)第一個調(diào)用的是NSArray
結(jié)論:NSArray
繼承于NSObject
饶号,系統(tǒng)初始化NSArray
的時候,會調(diào)用callAlloc
方法里的msgSend
季蚂,由于NSArray
沒有alloc
方法茫船,所以這個消息會發(fā)送給根父類NSObject
,得出NSObject
的初始化方法由系統(tǒng)幫我們執(zhí)行了扭屁。
- 第二個問題:為什么
+ (id)alloc {}
下面的方面明明是_objc_rootAlloc
透硝,它為什么跑去調(diào)用objc_alloc
?
進入LLVM開源代碼疯搅,搜索objc_alloc
會找到一個關(guān)鍵方法
結(jié)論:調(diào)用alloc
方法,在LLVM層(在編譯啟動就已完成)埋泵,對alloc
進行了修飾幔欧,會指向objc_alloc
方法
- 第三個問題:那為什么執(zhí)行完
objc_alloc
方法后罪治,又會執(zhí)行一次alloc
->_objc_rootAlloc
方法呢?
- 第一次調(diào)用
objc_alloc
方法礁蔗,會執(zhí)行到msgSend
觉义,進入LLVM源碼查看
- 第一次是調(diào)用if里面的條件判斷,執(zhí)行
tryGenerateSpecializedMessageSend
,傳入的Sel
為objc_alloc
浴井,所以程序第一次走了objc_alloc
晒骇。 - 返回一個NO,接著執(zhí)行下面的
GenerateMessageSend
磺浙,傳入的Sel
為alloc
洪囤,所以第二次走到了alloc
->_objc_rootAlloc
- 第四個問題: 第二次初始化
GomuPerson
對象流程alloc
->objc_alloc
->callAlloc
->_objc_rootAllocWithZone
為什么第二次初始化對象的時候就不調(diào)用_objc_rootAlloc
了呢?
- 先回顧一下第一次初始化對象的流程
alloc
->objc_alloc
->callAlloc
->objc_msgSend
->alloc
->_objc_rootAlloc
->callAlloc
->_objc_rootAllocWithZone
- 對比第一次和第二次發(fā)現(xiàn)撕氧,第一次調(diào)用
objc_alloc
走到了callAlloc
的objc_msgSend
方法瘤缩,第一次調(diào)用objc_alloc
走到了callAlloc
的_objc_rootAllocWithZone
方法。 - 差異在哪伦泥?第二次初始化對象剥啤,不會調(diào)用
objc_msgSend
方法。 - 那我們現(xiàn)在探索就變成
objc_msgSend
方法做了什么不脯。 -
objc_msgSend
就是通過sel
去找Imp
府怯,找到之后cache,既然有了cache防楷,那我們第二次初始化GomuPerson對象的時候牺丙,就直接可以進入快速查詢,直接找cache域帐。(該部分內(nèi)容屬于<OC方法底層原理>赘被,后面我們會專門開一期來闡述)。