iOS底層原理08:類結(jié)構(gòu)分析——bits屬性

iOS底層原理07:類 & 類結(jié)構(gòu)分析中我們對類結(jié)構(gòu)有了大概的認識,本文主要探索objc_classbits屬性,探索成員變量遮晚、屬性梯影、方法(對象方法曾掂、類方法)歧蕉、協(xié)議等是如何存儲的

【W(wǎng)WDC2020】類數(shù)據(jù)結(jié)構(gòu)的優(yōu)化

WWDC2020中關于數(shù)據(jù)結(jié)構(gòu)的變化(Class data structures changes)視頻地址
Object-C運行時會使用這些數(shù)據(jù)結(jié)構(gòu)來跟蹤類汰蓉,??下面我們先來了解 Clean MemoryDirty Memory的區(qū)別委煤,方便我們更好得理解類數(shù)據(jù)結(jié)構(gòu)的優(yōu)化

Clean Memory 和 Dirty Memory的區(qū)別

Clean Memory

  • clean memory是指加載后不會發(fā)生更改的內(nèi)存
  • class_ro_t 就屬于clean memory堂油,因為它是只讀的
  • clean memory可以進行移除,從而節(jié)省更多的內(nèi)存空間素标,因為如果你需要clean memory称诗,系統(tǒng)可以從磁盤中重新加載

Dirty Memory

  • dirty memory是指在進程運行時會發(fā)生更改的內(nèi)存
  • 類結(jié)構(gòu)一經(jīng)使用就會變成dirty memory,因為運行時會向它寫入新的數(shù)據(jù)头遭。(例如:創(chuàng)建一個新的方法緩存寓免,并從類中指向它)
  • dirty memoryclean memory要昂貴得多,只要進程在運行计维,它就必須一直存在
  • macOS可以選擇換出dirty memory袜香,但因為iOS不使用swap,所以dirty memory在iOS中代價很大

因此鲫惶,蘋果為了性能優(yōu)化蜈首,類數(shù)據(jù)被分成兩部分,可以保持清潔的數(shù)據(jù)越多越好,通過分離出那些永遠不會更改的數(shù)據(jù)(即class_ro_t)欢策,可以把大部分的類數(shù)據(jù)存儲為clean memory

類結(jié)構(gòu)的優(yōu)化

雖然class_ro_t這些數(shù)據(jù)足夠我們使用類吆寨,但因為OC的動態(tài)特性,運行時需要跟蹤每個類的更多信息踩寇,所以當一個類首次被使用啄清,runtime會為它分配額外的存儲空間

  • 這個運行時分配的存儲容量是class_rw_t俺孙,用于讀取-編寫數(shù)據(jù)辣卒,在這個數(shù)據(jù)結(jié)構(gòu)中,我們存儲了只有在運行時才會生成的新消息

優(yōu)化之前睛榄,類結(jié)構(gòu)如下??


image.png

所有的都會鏈接成一個樹狀結(jié)構(gòu)荣茫,通過使用First SubclassNext Sibling Class指針實現(xiàn)的,這允許運行時遍歷當前使用的所有類场靴。

【問題】 為什么class_rw_tclass_ro_t中都存在方法啡莉、屬性呢?

  • class_rw_t可以在運行時進行更改
  • category被加載時憎乙,它可以向類中添加新的方法
  • 我們還可以使用運行時 API動態(tài)添加方法票罐,如method_setImplementation
  • class_ro_t 是只讀的,所以我們需要在class_rw_t中來跟蹤這些東西

在任何給定的設備中,都有許多類在使用腕扶,蘋果開發(fā)人員在iPhone上的整個系統(tǒng)中測量了斤蔓,class_rw_t結(jié)構(gòu)占用了相當多的內(nèi)存,【切記】我們在讀取-編寫部分需要這些東西鸟蟹,因為他們可以在運行時更改。
【問題】如果縮小class_rw_t的結(jié)構(gòu)呢?

  • 蘋果開發(fā)人員發(fā)現(xiàn)梢什,大約只有10%的類真正地更改了他們的方法
  • 而且只有Swift類會使用這個demangled name字段(只有訪問它們Objective-C名稱時才需要

所以我們可以拆掉那些平時不用的部分,以達到內(nèi)存優(yōu)化朝聋,如下圖所示:

image.png

  • 這樣class_rw_t的大小會減少一半
  • 對于那些確實需要額外信息嗡午,才會有分配這些擴展記錄,即多了一層class_rw_ext_t數(shù)據(jù)冀痕,而剩下90%的類荔睹,從來不需要這些擴展數(shù)據(jù),也就沒有class_rw_ext_t這層數(shù)據(jù)結(jié)構(gòu)

我們可以通過heap來檢查正在運行的進程所使用的堆內(nèi)存

//查看微信進程言蛇,在活動監(jiān)視器中查看僻他,微信進程=591
heap 591 | egrep "class_rw|COUNT|class_ro"
image.png

總結(jié)

class_rw_t優(yōu)化,其實就是對class_rw_t不常用的部分進行了剝離腊尚。如果需要用到這部分就從擴展記錄中分配一個吨拗,滑到類中供其使用。現(xiàn)在大家對類應該有個更清楚的認識。

lldb調(diào)試分析

準備工作

定義兩個類

  • 繼承自NSObject的類HTPerson
@interface HTPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
+ (void)sayBye;
@end

@implementation HTPerson
- (void)sayHello {
    NSLog(@"%@",__func__);
}
+ (void)sayBye {
    NSLog(@"%@",__func__);
}
@end
  • 繼承自HTPerson的類HTTeacher
@interface HTTeacher : HTPerson
@end

@implementation HTTeacher
@end
  • main.m中代碼如下
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HTPerson *p1 = [HTPerson alloc];
        HTPerson *p2 = [[HTPerson alloc] init];
        HTTeacher *t = [[HTTeacher alloc] init];
        NSLog(@"end--%@--%@", @"123", p1);
    }
    return 0;
}

lldb調(diào)試class_rw_t結(jié)構(gòu)

斷點調(diào)試步驟如下

  • 獲取類的首地址:p/x HTPerson.class

  • 獲取bits的地址:類首地址 + 0x20劝篷,p/x (0x00000001000082f8 + 0x20)

  • 通過bits->data()獲取class_rw_t結(jié)構(gòu)體地址

  • 打印class_rw_t結(jié)構(gòu)體數(shù)據(jù)

  • 【1】執(zhí)行完HTPerson *p1 = [HTPerson alloc];哨鸭,查看class_rw_t數(shù)據(jù),發(fā)現(xiàn)witness的值為0娇妓,firstSubclass的值為nil

image.png
  • 【2】執(zhí)行完HTPerson *p2 = [[HTPerson alloc] init];兔跌,查看class_rw_t數(shù)據(jù),發(fā)現(xiàn)witness的值變?yōu)?code>1
image.png
  • 【3】執(zhí)行完HTTeacher *t = [[HTTeacher alloc] init];峡蟋,即使用子類坟桅,發(fā)現(xiàn)firstSubclass的值變成了HTTeacher
image.png

屬性探究

  • class_rw_t結(jié)構(gòu)體提供了properties()函數(shù)來獲取類的屬性,得到property_array_t數(shù)據(jù)
image.png
  • 查看property_array_t數(shù)據(jù)結(jié)構(gòu)蕊蝗,發(fā)現(xiàn)它繼承自list_array_tt仅乓,并且有property_tproperty_list_t兩層數(shù)據(jù)
image.png
  • 繼續(xù)查看list_array_tt數(shù)據(jù)結(jié)構(gòu),發(fā)現(xiàn)內(nèi)部有個聯(lián)合體數(shù)據(jù)蓬戚,其中list屬性列表property_list_t的指針地址
image.png
  • 查看property_list_t數(shù)據(jù)結(jié)構(gòu)夸楣,繼承自entsize_list_tt,提供了泛型模版數(shù)據(jù)
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
  • 繼續(xù)查看entsize_list_tt數(shù)據(jù)結(jié)構(gòu)子漩,終于找到了get()方法(獲取對應index位置的屬性)
image.png

??通過lldb斷點來查看HTPerson的屬性

image.png

通過class_rw_t -> properties()獲取的屬性列表豫喧,只存儲了兩個屬性:nameage
【問題】我們聲明的變量-hobby保存在哪里呢?

成員變量探究

屬性列表中沒有存儲變量幢泼,觀察發(fā)現(xiàn)class_rw_t還有一個獲取class_ro_t *的方法const class_ro_t *ro() const {}紧显,成員變量會不會在class_ro_t中,源碼查看class_ro_t結(jié)構(gòu)體定義

    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
    // ...省略

class_ro_t是結(jié)構(gòu)體類型缕棵,有一個const ivar_list_t * ivars;變量孵班。從名字我們可以猜到里面應該存儲變量。??通過lldb驗證如下圖:

image.png

總結(jié)

  • 變量的底層實現(xiàn)是ivar_t招驴,存儲在class_ro_t中的變量列表ivars
  • 系統(tǒng)會給屬性自動生成一個帶_屬性名變量篙程,存儲在class_ro_t的變量列表中

方法探究

實例方法探究

lldb調(diào)試如下??


image.png
  • 類的對象方法列表通過bits->data()->methods()獲取
  • 類的對象方法列表在底層結(jié)構(gòu)是method_list_t
  • p $7.get(index)在方法列表中獲取不到具體的值,因為method_t中進行了處理别厘,需要通過big()獲取方法
  • 類的方法列表中沒有類方法

從上圖打印結(jié)構(gòu)可以看出虱饿,類會為屬性提供默認的set、get方法触趴,
但是我們沒有發(fā)現(xiàn)HTPerson的類方法+ (void)sayBye;

類方法探究

對象的方法是存儲在中氮发,那么類方法可能存儲在元類中。按照這個思路探究下

image.png

從打印結(jié)果可以得知:類方法存儲在元類方法列表

協(xié)議探索

  • 新增一個協(xié)議HTPersonProtocol雕蔽,讓HTPerson遵守協(xié)議
@protocol HTPersonProtocol <NSObject>
- (void)protocolMethod1;
- (void)protocolMethod2;
@end

@interface HTPerson : NSObject<HTPersonProtocol>
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@property(nonatomic, assign) NSInteger age;
- (void)sayHello;
+ (void)sayBye;
@end
image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末折柠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子批狐,更是在濱河造成了極大的恐慌扇售,老刑警劉巖前塔,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異承冰,居然都是意外死亡华弓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門困乒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寂屏,“玉大人,你說我怎么就攤上這事娜搂∏” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵百宇,是天一觀的道長考廉。 經(jīng)常有香客問我,道長携御,這世上最難降的妖魔是什么昌粤? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮啄刹,結(jié)果婚禮上涮坐,老公的妹妹穿的比我還像新娘。我一直安慰自己誓军,他們只是感情好袱讹,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谭企,像睡著了一般廓译。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上债查,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音瓜挽,去河邊找鬼盹廷。 笑死,一個胖子當著我的面吹牛久橙,可吹牛的內(nèi)容都是我干的俄占。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼淆衷,長吁一口氣:“原來是場噩夢啊……” “哼缸榄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祝拯,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤甚带,失蹤者是張志新(化名)和其女友劉穎她肯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鹰贵,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡晴氨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了碉输。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片籽前。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖敷钾,靈堂內(nèi)的尸體忽然破棺而出枝哄,到底是詐尸還是另有隱情,我是刑警寧澤阻荒,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布膘格,位于F島的核電站,受9級特大地震影響财松,放射性物質(zhì)發(fā)生泄漏瘪贱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一辆毡、第九天 我趴在偏房一處隱蔽的房頂上張望菜秦。 院中可真熱鬧,春花似錦舶掖、人聲如沸球昨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽主慰。三九已至,卻和暖如春鲫售,著一層夾襖步出監(jiān)牢的瞬間共螺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工情竹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留藐不,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓秦效,卻偏偏與公主長得像雏蛮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子阱州,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容