iOS之深入解析alloc、init与new的底层原理
底層探索
一、對(duì)于iOS的底層原理探索,查找到函數(shù)所在的源碼庫(kù),一般有以下方法實(shí)現(xiàn)
① 符號(hào)斷點(diǎn)
- 在哪里新建符號(hào)斷點(diǎn):選擇 Symbolic Breakpoint :
- 怎么添加符號(hào)斷點(diǎn):在Symbol中加入 alloc、objc_alloc、_objc_rootAlloc 等:
- 繼續(xù)追蹤方法底層原理,可以繼續(xù)使用以上符號(hào)斷點(diǎn)方法添加對(duì)應(yīng)的方法名即可;
- 直到最后追蹤到系統(tǒng)底層庫(kù)為止,例如: alloc 的源碼最后追蹤到位于 libobjc.A.dylib 庫(kù)。
② LLDB調(diào)試
- 在需要探索的內(nèi)部方法或者實(shí)現(xiàn)的地方,打上斷點(diǎn);
- 運(yùn)行代碼,執(zhí)行到斷點(diǎn)地方,然后按住 control 鍵,即出現(xiàn)以下調(diào)試斷點(diǎn):
- 點(diǎn)擊上圖中的第四個(gè)step into之后,即可看見(jiàn)相應(yīng)實(shí)現(xiàn)方法,如:alloc 如下:
- 再添加一個(gè) objc_alloc 符號(hào)斷點(diǎn),便可顯示 objc_alloc 所在的源碼庫(kù) libobjc.A.dylib 。
③ 匯編分析
- xcode 工具欄選擇 Xcode -> Debug WorkFlow -> Always Show Disassembly ,該選項(xiàng)表示始終顯示反匯編 ,即匯編分析
- 按照“符號(hào)斷點(diǎn)”的運(yùn)行代碼,執(zhí)行到斷點(diǎn)地方,然后按住 control鍵,即出現(xiàn)以下調(diào)試斷點(diǎn)方法,即可進(jìn)入callq ,對(duì)應(yīng) objc_alloc:
- 最后,也可以看到 alloc 對(duì)應(yīng)的源碼庫(kù)為 libobjc.A.dylib 庫(kù)。
二、作為 iOS 開(kāi)發(fā)者,最需要關(guān)注的應(yīng)該就是從應(yīng)用啟動(dòng)到應(yīng)用被 kill 掉這一整個(gè)生命周期的內(nèi)容。不妨從最熟悉的 main 函數(shù)開(kāi)始,一般來(lái)說(shuō),在 main.m 文件中打一個(gè)斷點(diǎn),左側(cè)的調(diào)用堆棧視圖應(yīng)該如下圖所示:
三、調(diào)用堆棧有兩個(gè)注意點(diǎn):
- 需要關(guān)閉 Xcode 左側(cè) Debug 區(qū)域最下面的 show only stack frames with debug symbols and between libraries
- 增加一個(gè) _objc_init 的符號(hào)斷點(diǎn)
四、通過(guò)上面的調(diào)用堆棧信息不難得出一個(gè)簡(jiǎn)單粗略的加載流程
五、Apple 開(kāi)源庫(kù)源碼
- Apple 所有開(kāi)源源碼匯總地址,根據(jù)相應(yīng)的版本查找對(duì)應(yīng)的源碼,以mac 10.15為例: macOS --> 10.15 --> 選擇10.15 --> 搜索 objc;
- Apple 比較直接的源碼下載地址,直接搜索想要下載的源碼名稱(chēng)即可,例如objc:直接搜索 objc --> objc4/ --> 選擇相應(yīng)的objc的版本。
內(nèi)存對(duì)齊
一、內(nèi)存對(duì)齊概念
① 什么是內(nèi)存對(duì)齊?
- 內(nèi)存對(duì)齊是一種在計(jì)算機(jī)內(nèi)存中 排列數(shù)據(jù) (表現(xiàn)為變量的地址)、 訪(fǎng)問(wèn)數(shù)據(jù) (表現(xiàn)為CPU讀取數(shù)據(jù))的一種方式。
- 內(nèi)存對(duì)齊包含了兩種相互獨(dú)立又相互關(guān)聯(lián)的部分: 基本數(shù)據(jù)對(duì)齊和結(jié)構(gòu)體數(shù)據(jù)對(duì)齊 。
② 為什么要進(jìn)行內(nèi)存對(duì)齊?
- 平臺(tái)原因(移植原因):不是所有的硬件平臺(tái)都能訪(fǎng)問(wèn)任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類(lèi)型的數(shù)據(jù),否則拋出硬件異常。
- 性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在 自然邊界上對(duì)齊 。原因在于,為了訪(fǎng)問(wèn)未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪(fǎng)問(wèn);而對(duì)齊的內(nèi)存訪(fǎng)問(wèn)僅需要 一次訪(fǎng)問(wèn) 。
③ 內(nèi)存對(duì)齊原則
在 iOS 中,對(duì)象的屬性需要進(jìn)行內(nèi)存對(duì)齊,而對(duì)象本身也需要進(jìn)行內(nèi)存對(duì)齊。內(nèi)存對(duì)齊有三原則:
- 數(shù)據(jù)成員對(duì)齊原則 :結(jié)構(gòu)( struct )(或聯(lián)合( union ))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在 offset 為 0 的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大小。
- 結(jié)構(gòu)體作為成員 :如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開(kāi)始存儲(chǔ)。(如:struct a?存有struct b,b?有char、int 、double等元素,那b應(yīng)該從8的整數(shù)倍開(kāi)始存儲(chǔ))
- 收尾工作:結(jié)構(gòu)體的總大小,也就是 sizeof 的結(jié)果,必須是其 內(nèi)部最大成員的整數(shù)倍 ,不足的要補(bǔ)?。
簡(jiǎn)而言之:
- 前面的地址必須是后面的地址正數(shù)倍,不是就補(bǔ)齊;
- 結(jié)構(gòu)體里面的嵌套結(jié)構(gòu)體大小要以該嵌套結(jié)構(gòu)體最大元素大小的整數(shù)倍;
- 整個(gè) Struct 的地址必須是最大字節(jié)的整數(shù)倍。
注意:
- 在字節(jié)對(duì)齊算法中,對(duì)齊的主要是對(duì)象,而對(duì)象的本質(zhì)則是一個(gè) struct objc_object 的結(jié)構(gòu)體;
- 結(jié)構(gòu)體在內(nèi)存中是 連續(xù)存放 的,所以可以利用這點(diǎn)對(duì)結(jié)構(gòu)體進(jìn)行強(qiáng)轉(zhuǎn);
- 蘋(píng)果早期是8字節(jié)對(duì)齊,現(xiàn)在是 16字節(jié)對(duì)齊 。
二、對(duì)象申請(qǐng)內(nèi)存和系統(tǒng)開(kāi)辟內(nèi)存
在Xocde中打印以下兩個(gè)函數(shù):
Boy *boy = [Boy alloc];NSLog(@"%lu - %lu - %lu", sizeof(boy), class_getInstanceSize([Boy class]), malloc_size((__bridge const void *)(boy)));- 可以發(fā)現(xiàn)對(duì)象自己申請(qǐng)的內(nèi)存大小與系統(tǒng)實(shí)際給開(kāi)辟的大小時(shí)不一樣的,這里對(duì)象申請(qǐng)的內(nèi)存大小是 40 個(gè)字節(jié),而系統(tǒng)開(kāi)辟的是 48 個(gè)字節(jié)。
- 40 個(gè)字節(jié)不難理解,是因?yàn)楫?dāng)前對(duì)象有 4 個(gè)屬性,有三個(gè)屬性為 8 個(gè)字節(jié),有一個(gè)屬性為 4個(gè)字節(jié),再加上 isa 的 8 個(gè)字節(jié),就是 32 + 4 = 36 個(gè)字節(jié),然后根據(jù)內(nèi)存對(duì)齊原則,36 不能被 8 整除,36 往后移動(dòng)剛好到了 40 就是 8 的倍數(shù),所以?xún)?nèi)存大小為 40。
- class_getInstanceSize 和 malloc_size 對(duì)同一個(gè)對(duì)象返回的結(jié)果不一樣的,原因是 malloc_size 是直接返回的 calloc 之后的指針的大小。
- 通過(guò)instanceSize計(jì)算的內(nèi)存大小,向內(nèi)存中申請(qǐng)大小為 size 的內(nèi)存,并賦值給 obj ,因此 obj 是指向內(nèi)存地址的指針;在未執(zhí)行 calloc 時(shí),po obj 為 nil,執(zhí)行后,再 po obj ,返回一個(gè)16進(jìn)制的地址。
- 而 class_getInstanceSize 內(nèi)部實(shí)現(xiàn)是:也就是說(shuō) class_getInstanceSize 會(huì)輸出 8 個(gè)字節(jié),malloc_size 會(huì)輸出 16 個(gè)字節(jié),當(dāng)然前提是該對(duì)象沒(méi)有任何屬性。
三、不同數(shù)據(jù)類(lèi)型占據(jù)的內(nèi)存大小(字節(jié))
| bool | BOOL(64位) | 1 | 1 |
| signed char | (_signed char)int8_t、BOOL(32位) | 1 | 1 |
| unsigned char | Boolean | 1 | 1 |
| short | int16_t | 2 | 2 |
| unsigned short | unichar | 2 | 2 |
| int、int32_t | NSInteger(32位)、boolean_t(32位) | 4 | 4 |
| unsigned int | NSUInteger(32位)、boolean_t(64位) | 4 | 4 |
| long | NSInteger(64位) | 4 | 8 |
| unsigned long | NSUInteger(64位) | 4 | 8 |
| long long | int64_t | 8 | 8 |
| float | CGFloat(32位) | 4 | 4 |
| double | CGFloat(64位) | 8 | 8 |
alloc、init與new的底層原理
一、實(shí)例初始化
運(yùn)用Objective-C語(yǔ)言進(jìn)行開(kāi)發(fā)的時(shí)候,我們都知道可以通過(guò) [XXX alloc]、[[XXX alloc] init]、[XXX new]的形式進(jìn)行對(duì)象實(shí)例的創(chuàng)建,那么不禁會(huì)疑惑alloc、init、new它們各自都做了什么呢?同樣的都是進(jìn)行實(shí)例創(chuàng)建,它們之間有什么內(nèi)在的關(guān)聯(lián)呢?它們之間又有著什么樣的區(qū)別呢?
- 我們不妨先出使用alloc、init、new分別初始化對(duì)象,具體實(shí)現(xiàn)和并打印對(duì)象結(jié)果、指針地址、內(nèi)存地址如下:
- 通過(guò)以上可以看出:boy1與boy2是同一個(gè)對(duì)象,而與boy3、boy4則是不同的對(duì)象。雖然boy1與boy2是同一個(gè)對(duì)象,并且指針地址相同,指向的是同一個(gè)內(nèi)存空間,但是它們的內(nèi)存地址卻又不一樣,那么alloc、init、new到底做了什么呢?其實(shí),當(dāng)alloc執(zhí)行返回的時(shí)候,x0寄存器就會(huì)存儲(chǔ)一個(gè)指針,指向申請(qǐng)的內(nèi)存空間,并且可以看到內(nèi)存地址相差8個(gè)字節(jié)。
二、如何查找alloc實(shí)現(xiàn)?
- 在alloc初始化的地方,打上斷點(diǎn),如下:
- 運(yùn)行代碼,執(zhí)行到斷點(diǎn)地方,然后按住 control鍵,即出現(xiàn)以下調(diào)試斷點(diǎn):
- 按住control,然后點(diǎn)擊第四個(gè)圖標(biāo)調(diào)試,就可以進(jìn)入到 objc_alloc 中;
- symbolic Beakpoint 添加符號(hào)斷點(diǎn) objc_alloc 可以發(fā)現(xiàn)在 libobjc.A.dylib ;
- 也可以先把斷點(diǎn)打在alloc的地方,運(yùn)行,斷點(diǎn)斷住之后再下一個(gè)alloc的符號(hào)斷點(diǎn),會(huì)發(fā)現(xiàn)有很多的類(lèi)實(shí)現(xiàn)了alloc方法,先不用管直接過(guò)掉斷點(diǎn),就會(huì)進(jìn)入?yún)R編的代碼之中;
- 可以通過(guò) Xcode -> Debug 進(jìn)入?yún)R編分析:
三、底層實(shí)現(xiàn)源碼分析
既然是深入底層,那么肯定需要知道底層代碼是做了什么事情,蘋(píng)果是開(kāi)源了這部分的代碼,可以在Source Browser下載。下載下來(lái)的源碼直接編譯是通不過(guò)的,需要自己修改下配置,具體請(qǐng)參考o(jì)bjc_debug。
① alloc
- 進(jìn)入到 alloc 的源碼里面,我們發(fā)現(xiàn) alloc 調(diào)用了 _objc_rootAlloc 方法,而_objc_rootAlloc調(diào)用了 callAlloc 方法。
- 在callAlloc方法里面可以看到if的判斷條件,那么 fastpath(!cls->ISA()->hasCustomAWZ()) 都做了什么呢?fastpath又是什么呢?fastpath的定義如下:
- 要搞清楚fastpath是什么,就要知道 __builtin_expect 是什么。其實(shí),這個(gè)指令是 gcc 引入的,作用是允許程序員將最有可能執(zhí)行的分支告訴編譯器。這個(gè)指令的寫(xiě)法為: __builtin_expect(EXP, N) 。
- 目的:編譯器可以對(duì)代碼進(jìn)行優(yōu)化,以 減少指令跳轉(zhuǎn)帶來(lái)的性能下降 ,即性能優(yōu)化;
- 作用:允許程序員 將最有可能執(zhí)行的分支告訴編譯器 ;
- 指令的寫(xiě)法為: __builtin_expect(EXP, N) ,表示 EXP==N的概率很大;
- fastpath定義中__builtin_expect((x),1)表示 x 的值為真的可能性更大;即執(zhí)行 if 里面語(yǔ)句的機(jī)會(huì)更大;
- slowpath定義中的 __builtin_expect((x),0) 表示 x 的值為假的可能性更大,即執(zhí)行 else 里面語(yǔ)句的機(jī)會(huì)更大;
- !cls->ISA()->hasCustomAWZ() 做了什么呢?很明顯是調(diào)用了 hasCustomAWZ 這樣一個(gè)方法:
- RW_HAS_DEFAULT_AWZ 這個(gè)是用來(lái)標(biāo)示當(dāng)前的 class 或者是 superclass 是否有默認(rèn)的 alloc/allocWithZone: 。值得注意的是,這個(gè)值會(huì)存儲(chǔ)在 metaclass 中。
- hasDefaultAWZ( ) 方法是用來(lái)判斷當(dāng)前 class 是否有 重寫(xiě)allocWithZone 。如果 cls->ISA()->hasCustomAWZ() 返回YES,意味著當(dāng)前的class有重寫(xiě) allocWithZone方法 ,那么就直接對(duì)class進(jìn)行 allocWithZone,申請(qǐng)內(nèi)存空間 。
- allocWithZone 內(nèi)部調(diào)用了 _objc_rootAllocWithZone 方法,接下來(lái)分析下_objc_rootAllocWithZone方法
- 不難發(fā)現(xiàn)直接調(diào)用了 class_createInstance 方法來(lái)創(chuàng)建對(duì)象,而 _class_createInstanceFromZone 是 alloc 源碼的核心操作,實(shí)現(xiàn)主要分為三部分:
- cls->instanceSize :計(jì)算需要開(kāi)辟的內(nèi)存空間大小
- calloc :申請(qǐng)內(nèi)存,返回地址指針;
- obj->initInstanceIsa :將 類(lèi) 與 isa 關(guān)聯(lián)。
- 創(chuàng)建對(duì)象就要為對(duì)象開(kāi)辟內(nèi)存空間,這里會(huì)不會(huì)就是為對(duì)象開(kāi)辟了空間呢?我們發(fā)現(xiàn)方法里面調(diào)用了 instanceSize方法 ,這個(gè)是不是就是開(kāi)辟內(nèi)存空間的方法呢?
- 不難看出,instanceSize方法計(jì)算出了對(duì)象所需內(nèi)存的大小,而且必須是大于或等于16字節(jié),然后調(diào)用calloc函數(shù)為對(duì)象分配內(nèi)存空間。通過(guò) obj = (id)calloc(1, size) 創(chuàng)建 obj 對(duì)象傳遞了一個(gè) size ,就是 obj 的內(nèi)存大小對(duì),對(duì)于alloc申請(qǐng)內(nèi)存,任何集成 NSObject 對(duì)象創(chuàng)建之后默認(rèn)的內(nèi)存大小為16個(gè)字節(jié);
- 那 initInstanceIsa 方法又是干什么的呢?其實(shí)這個(gè)方法就是初始化isa指針。
- 上面我們分析了 hasDefaultAWZ( ) 方法返回Yes的情況,那如果hasDefaultAWZ( )方法返回NO呢?以下是 hasDefaultAWZ( ) 返回 NO 的情況,有去判斷當(dāng)前的class是否支持快速alloc。如果可以,直接調(diào)用calloc函數(shù),并且申請(qǐng)一塊 bits.fastInstanceSize() 大小的內(nèi)存空間,然后初始化isa指針,否則直接調(diào)用class_createInstance方法。
- fastInstanceSize 會(huì)執(zhí)行到align16
- 然后 align16 是16字節(jié)對(duì)齊算法:
- 總結(jié):alloc為我們創(chuàng)建了一個(gè)對(duì)象并且申請(qǐng)了一塊不少于16字節(jié)的內(nèi)存空間,并初始化isa指針。
② init
既然 alloc 創(chuàng)建了對(duì)象,那還要 init 干嘛呢?init 又做了什么呢?
- init 源碼如下:
- 不難看出,init并沒(méi)有做什么,只是把當(dāng)前的對(duì)象返回了。既然什么都沒(méi)做那我們還需要調(diào)用 init 嗎?答案是肯定的,其實(shí) init 就是一個(gè)工廠(chǎng)模式,方便開(kāi)發(fā)者自行重寫(xiě)定義,即交給子類(lèi)可以自定義去重寫(xiě)。
③ new
- new底層源碼
- new調(diào)用的是callAlloc方法和init,那么可以理解為new實(shí)際上就是alloc+init的綜合體。
總結(jié)
- alloc 創(chuàng)建了對(duì)象并且申請(qǐng)了一塊不少于 16 字節(jié)的內(nèi)存空間,并初始化了 isa 指針;
- 相比于 alloc 來(lái)說(shuō), init 內(nèi)部實(shí)現(xiàn)十分簡(jiǎn)單,先來(lái)到的是 _objc_rootInit ,然后就直接返回 obj 了。其實(shí)這里是一種抽象工廠(chǎng)設(shè)計(jì)模式的體現(xiàn),對(duì)于 NSObject 自帶的 init 方法來(lái)說(shuō),其實(shí)啥也沒(méi)干,但是如果你繼承于 NSObject 的話(huà),然后就可以去重寫(xiě) initWithXXX 之類(lèi)的初始化方法來(lái)做一些初始化操作;
- new 其實(shí)是 alloc+init 的一個(gè)綜合使用。
總結(jié)
以上是生活随笔為你收集整理的iOS之深入解析alloc、init与new的底层原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: iOS之深入解析dispatch sou
- 下一篇: Swift之捕捉侧滑返回事件并跳转指定控