iOS底層原理07:類 & 類結(jié)構(gòu)分析中我們對類結(jié)構(gòu)
有了大概的認識,本文主要探索objc_class
的bits屬性
,探索成員變量
遮晚、屬性
梯影、方法
(對象方法曾掂、類方法)歧蕉、協(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 Memory
和Dirty 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 memory
比clean 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)如下??
所有的類
都會鏈接成一個樹狀結(jié)構(gòu)
荣茫,通過使用First Subclass
和Next Sibling Class
指針實現(xiàn)的,這允許運行時遍歷當前使用的所有類场靴。
【問題】 為什么class_rw_t
和class_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)化朝聋,如下圖所示:
- 這樣
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"
總結(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
- 【2】執(zhí)行完
HTPerson *p2 = [[HTPerson alloc] init];
兔跌,查看class_rw_t
數(shù)據(jù),發(fā)現(xiàn)witness
的值變?yōu)?code>1
- 【3】執(zhí)行完
HTTeacher *t = [[HTTeacher alloc] init];
峡蟋,即使用子類坟桅,發(fā)現(xiàn)firstSubclass
的值變成了HTTeacher
屬性探究
-
class_rw_t
結(jié)構(gòu)體提供了properties()函數(shù)
來獲取類的屬性,得到property_array_t
數(shù)據(jù)
- 查看
property_array_t
數(shù)據(jù)結(jié)構(gòu)蕊蝗,發(fā)現(xiàn)它繼承自list_array_tt
仅乓,并且有property_t
和property_list_t
兩層數(shù)據(jù)
- 繼續(xù)查看
list_array_tt
數(shù)據(jù)結(jié)構(gòu),發(fā)現(xiàn)內(nèi)部有個聯(lián)合體數(shù)據(jù)蓬戚,其中list
即屬性列表property_list_t
的指針地址
- 查看
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位置的屬性)
??通過lldb斷點來查看HTPerson
的屬性
通過class_rw_t -> properties()
獲取的屬性列表豫喧,只存儲了兩個屬性:name
和age
【問題】我們聲明的變量-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驗證如下圖:
總結(jié)
-
變量
的底層實現(xiàn)是ivar_t
招驴,存儲在class_ro_t
中的變量列表ivars
里 - 系統(tǒng)會給
屬性
自動生成一個帶_屬性名
變量篙程,存儲在class_ro_t
的變量列表中
方法探究
實例方法探究
lldb調(diào)試如下??
- 類的
對象方法列表
通過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;
類方法探究
對象的方法
是存儲在類
中氮发,那么類方法
可能存儲在元類
中。按照這個思路探究下
從打印結(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