WWDC2020對runtime的優(yōu)化
- 視頻的觀看地址:https://developer.apple.com/videos/play/wwdc2020/10163/ (最好用Safari瀏覽器打開)
- LLVM源碼地址:https://github.com/apple/llvm-project
看完視頻后總結(jié):本次改動不需要改動任何代碼,也不用學(xué)習(xí)新的API,這次主要是runtime關(guān)于內(nèi)存的優(yōu)化。在這種環(huán)境下我們不用改APP也會運(yùn)行得比之前更快更高效。
類的數(shù)據(jù)結(jié)構(gòu)
在《類的探究分析》一文中就詳細(xì)地解讀了類的結(jié)構(gòu)。在APP編譯成二進(jìn)制文件中,類的數(shù)據(jù)結(jié)構(gòu)發(fā)生了變化,其中包含了最常被訪問的信息,指向元類、父類和方法緩存的指針。以下是類的數(shù)據(jù)結(jié)構(gòu)圖:

Clean Memory 和 Dirty Memory的區(qū)別
Clean Memory
Clean Memory指的是在程序運(yùn)行中不會發(fā)生改變的內(nèi)存。class_ro_t就是屬于Clean Memory的,class_ro_t`內(nèi)存圖解:

-
clean memory加載后不會發(fā)生改變的內(nèi)存 -
class_ro_t就屬于clean memory,因為它是只讀的,不會對齊內(nèi)存進(jìn)行修改 -
clean memory是可以進(jìn)行移除的,從而節(jié)省更多的內(nèi)存空間,因為如果你有需要clean memory,系統(tǒng)可以從磁盤中重新加載
Dirty Memory
Dirty Memory指的是在程序運(yùn)行中會發(fā)生改變的內(nèi)存,也就是我們俗稱的臟數(shù)據(jù)。類的結(jié)構(gòu)一經(jīng)使用就會變成Dirty Memory,因為運(yùn)行時會向它寫入新的數(shù)據(jù)。例如往類中添加方法又或者加載類的子類或父類,這里指的是class_rw_t。class_rw_t內(nèi)存圖解如下:

-
Dirty Memory是這個類被分成兩部分的原因,可以保持類加載后不會發(fā)生更改的數(shù)據(jù)越多越好,通過分離永遠(yuǎn)不會更改的數(shù)據(jù),可以把大量的類數(shù)據(jù)存儲為Clean Memory。 -
class_rw_t(讀寫):類在加載的時候,屬性(properties)、協(xié)議(prococols)和方法(methods)會被運(yùn)行時動態(tài)的添加,也可以動態(tài)的修改(Method Swizzling)。所以類需要保存在class_rw_t中。 -
First Subclass、Next Subling Class:包含了運(yùn)行時才會生成的信息First Subclass、Next Subling Class,所有的類都會變成一個樹狀結(jié)構(gòu),就是通過First Subclass和Next Subling Class指針實現(xiàn)的,它允許運(yùn)行時遍歷當(dāng)前使用的所有類 -
Demangled Name:這個字段使用的頻率是比較少的,swift中才會使用。
總結(jié):
dirty memory要比clean memory更有價值而且要多,只要進(jìn)行運(yùn)行它就必須一直存在,通過分離出那些不會被改變的數(shù)據(jù),可以把大部分的類數(shù)據(jù)存儲為clean memory,這樣才能不斷的提高程序的性能。
class_rw_t 的優(yōu)化
dirty memory在類第一次加載的時候就一直存在,runtime會為它分配額外的內(nèi)存。運(yùn)行時分配的存儲容量時class_rw_t用于讀取-編寫數(shù)據(jù),但是dirty memory中仍然存在著比較多的clean memory,為了提高空間的利用率,拆分出更多的clean memory,減少dirty memory容量是比奴可少的。
第一步:拆分出class_ro_t,即運(yùn)行時不被修改的內(nèi)存。如下圖:

注意:由上圖發(fā)現(xiàn)一個疑點(diǎn),為什么方法,屬性在
class_ro_t中時,class_rw_t還要有方法,屬性呢?
- 屬性和方法在運(yùn)行時中有可能會
發(fā)生更改,這需要放在class_rw_t中。 - 在類加載的時候,可以往類中添加屬性和方法。
-
class_ro_t只是可讀的,需要放在class_rw_t中跟蹤類的相關(guān)信息。
第二步:拆分class_rw_t,提取其中的clean memory
在讀取-編寫屬性和方法的時候,只有10%的類都需要修改或者添加的,那么90%類可以說是不被修改的,那么就可以對class_rw_t進(jìn)行拆分,拆分如下:
class_rw_t拆分
這樣的話class_rw_t的大小就會減少一半,對于真的用到了被拆分出去的數(shù)據(jù)的時候,可以使用擴(kuò)展(extension)來完成這些,添加到類中供其使用(大約90%的類不需要這個擴(kuò)展)如下圖:
優(yōu)化后的結(jié)構(gòu)
總結(jié)
- 當(dāng)有類使用了
category的時候,那么此時的類就有了class_rw_t的結(jié)構(gòu),如果未使用分類,那么類就是一個單純的class_ro_t的結(jié)構(gòu)。 - 類結(jié)構(gòu)的優(yōu)化其實最要是分離出
class_ro_t和class_rw_t優(yōu)化,其實就是對class_rw_t不常用的部分進(jìn)行了剝離。如果需要用到這部分就從擴(kuò)展記錄中分配一個,滑到類中供其使用。
成員變量/實例變量和屬性的區(qū)別
《類的探究分析》一文中提到了成員變量存放在class_ ro_t中,那么我們用過查找objc4源碼得出下圖:

代碼層面探究:
@interface XXPerson : NSObject
{
int hobby; //成員變量
NSObject *objc; //實例變量
}
@property (nonatomic,strong) NSString *name; //屬性
@property (nonatomic,strong) NSString *nickName;
@property (nonatomic,assign) int age;
@end
通過xrun編譯成main.cpp文件,查看底層代碼:
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模擬器)-
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (手機(jī))
main.cpp分析
結(jié)論: - 屬性在編譯過程中自動加上
setter和getter方法。 - 屬性在底層編譯階段會變成
_方式的成員變量。
補(bǔ)充
官方類型編碼

注意:
- 編碼文檔存放在Apple Documents地址
-
Objective-C不支持long double類型,@encode(long double)返回d,和double類型的編碼值一樣。
案例分析:
案例分析
setName(v24@0:8@16)分析結(jié)果: -
v:void,代表無返回值。 -
24:setName函數(shù)的占用字節(jié)數(shù)。 -
@:參數(shù),id或者self。 -
0:從0號位置開始。 -
::SEL。 -
8:從8號位置開始。 -
@:參數(shù),setName。 -
16:從16號位置開始。
objc_setProperty與copy的關(guān)系
objc_setProperty方法相當(dāng)于一個中間層方法,主要是避免了每個類都調(diào)用底層的objc_setProperty方法。當(dāng)用copy關(guān)鍵字修飾屬性時,該屬性在編譯時候setter方法就會從定向到objc_setProperty方法,不像其他屬性·setter·方法使用首地址+內(nèi)存偏移的方式找到方法實現(xiàn)。
示例代碼:
@interface XJPerson : NSObject
@property (nonatomic,copy) NSString *name; //注意每個屬性的關(guān)鍵字
@property (nonatomic,strong) NSString *nickName;
@property (atomic,copy) NSString *address;
@property (atomic) NSString *school;
@end
編譯之后查看main.cpp,得到下圖:

結(jié)論:
- 使用
copy關(guān)鍵字修飾的屬性底層setter方法重定向到objc_setProperty方法 - 沒使用
copy關(guān)鍵字修飾的屬性底層setter方法通過首地址+內(nèi)存偏移來尋找并實現(xiàn)。
LLVM驗證對象屬性為copy時,setter方法的訪問
驗證流程圖:

LLVM源碼流程:
objc_setProperty -> getSetPropertyFn -> GetPropertySetFunction -> PropertyImplStrategy -> IsCopy(判斷copy關(guān)鍵字)結(jié)論:無論屬性是否是原子性還是非原子性的,用到copy關(guān)鍵字修飾的屬性setter方法底層都用
objc_setProperty實現(xiàn),strong關(guān)鍵字無法通過最后得判斷,需要通過首地址+內(nèi)存偏移的方式實現(xiàn)。
總結(jié):
底層代碼的分析需要很好的耐心,過程也是非常的枯燥。但是當(dāng)你弄明白了原理之后,就會發(fā)現(xiàn)知識是環(huán)環(huán)相扣的。非常高興自己又多了一分收獲,加油!



