類的內(nèi)存結(jié)構(gòu)優(yōu)化

WWDC2020對runtime的優(yōu)化

類的數(shù)據(jù)結(jié)構(gòu)

《類的探究分析》一文中就詳細(xì)地解讀了類的結(jié)構(gòu)。在APP編譯成二進(jìn)制文件中,類的數(shù)據(jù)結(jié)構(gòu)發(fā)生了變化,其中包含了最常被訪問的信息,指向元類、父類和方法緩存的指針。以下是類的數(shù)據(jù)結(jié)構(gòu)圖:

類的數(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)存圖解:

class_ro_t數(shù)據(jù)結(jié)構(gòu)

  • 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)存圖解如下:

class_rw_t數(shù)據(jù)結(jié)構(gòu)

  • 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 SubclassNext 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)存。如下圖:

提取lass_ro_t

注意:由上圖發(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_tclass_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é)論:
  • 屬性在編譯過程中自動加上settergetter方法。
  • 屬性在底層編譯階段會變成_方式的成員變量。

補(bǔ)充

官方類型編碼

官方類型編碼

注意:

  • 編碼文檔存放在Apple Documents地址
  • Objective-C 不支持long double類型,@encode(long double)返回d,和double類型的編碼值一樣。
    案例分析:
    案例分析

    setName(v24@0:8@16)分析結(jié)果:
  • vvoid,代表無返回值。
  • 24setName函數(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,得到下圖:

main.cpp源碼結(jié)果

結(jié)論:

  • 使用copy關(guān)鍵字修飾的屬性底層setter方法重定向到objc_setProperty方法
  • 沒使用copy關(guān)鍵字修飾的屬性底層setter方法通過首地址+內(nèi)存偏移來尋找并實現(xiàn)。

LLVM驗證對象屬性為copy時,setter方法的訪問

驗證流程圖:

LLVM驗證流程圖

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)相扣的。非常高興自己又多了一分收獲,加油!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容