iOS底層 -- 類的本質分析

1.本文概述

本文旨在經過 類&元類的建立時機類的結構及相關屬性添加的類信息等分析類在內存中的實際存在,並分享一些關於類的經典面試題


2.類&元類的建立時機

上文說到,對象經過isa和類關聯,同個類型的對象能夠屢次建立,因此對象能夠有多個。那麼類呢,根據開發經驗,很容易得出類在內存中只有一個,那究竟要怎麼實錘呢。提供驗證方式:html

  • command + b ,經過machoView查看

新建一個項目,並建立一個CJPerson類,在Products目錄下.app結尾的文件尚未編譯是報紅的,c++

command + b下變黑,Show In Finder後顯示包內容,把可執行文件拖入machoView
面試

machoView中顯示以下
緩存

能夠看到,在DATA段的_objc_classrefs內已經加載了CJPerson類,並指定了內存地址,說明類的建立是在編譯時期,而且只有一份。bash

元類也是在編譯期,由系統建立的,在個人理解,元類的做用是:
1.承接isa走向
2.緩存類方法
總以爲蘋果大費周章引入元類的概念,還有其餘的做用,至因而什麼,但願能夠指導我下
複製代碼

2.類的本質

main.m下初始化一個CJPerson對象數據結構

int main(int argc, const char * argv[]) {
    @autoreleasepool {        
        CJPerson * person = [[CJPerson alloc]init];        
        NSLog(@"%@",person.class);    
    }    
    return 0;
}複製代碼

clang編譯下main.m多線程

clang -rewrite-objc main.m -o main.cpp複製代碼

打開編譯後的main.cpp文件,直接來到最後面,會看到初始化時候是調用objc_getClassapp

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
         CJPerson * person = ((CJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((CJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJPerson"), sel_registerName("alloc")), sel_registerName("init"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_3995175d2_v6g81lknk8jdwh0000gn_T_main_e6fa2f_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("class")));
    }    
    return 0;
}複製代碼

在文件內搜索objc_getClass,看到ide

__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);複製代碼

再搜索objc_class,找到重定義post

typedef struct objc_class *Class;複製代碼

原來類是objc_class重定義的,感受這個objc_class很熟悉,好像在objc源碼裏面有看到過,那就搜索試試

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() {
         return bits.data();
    }
    ...
}複製代碼

順便貼出objc_object的結構

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};複製代碼
  • objc_class結構體內ISA是被註釋的,不表明結構體內不包含它,由於objc_class繼承objc_object,因此ISA是來自於父類
  • 一樣使用clang編譯下NSObject能夠獲得objc_object
  • isa的結構是isa_t,但是這裏使用class接收是能夠的,內部經過shift_class獲得class
總結:類的真正類型爲objc_class,繼承於objc_object(也就是NSObject的底層結構),說明萬物皆對象,類也是一個對象,CJPerson嚴格來講是類對象


3.類的數據結構

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}複製代碼

根據objc_class結構體能夠清楚看到,類有4個屬性,分別是isasuperclasscachebits

1.Class isa

isa在(iOS底層-一應俱全的isa)中已經分析過了,佔有8個字節,只是這裏的isa是指向元類

2.Class superclass

superclass就是指向的父類

typedef struct objc_class *Class;
複製代碼

superclass的結構一樣是objc_class,由於它是結構體指針,佔有8個字節

3.cache_t cache

cache是方法緩存,方法緩存涉及到方法查找流程緩存策略動態擴容等,下一章詳細說明,先看下cache_tbucket_t結構

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}複製代碼

struct bucket_t {
private:
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif
...
}複製代碼
  • buckets是裝方法的桶子,裏面放着方式實現imp,根據方法編號sel生成的key

  • mask是擴容因子,決定擴容的時機

  • occupied是當前佔用的容量

  • bucket_t 是結構體指針,佔用8個字節,mask_t就是int,佔用4個字節,因此maskoccupied各佔4個字節

  • cache_t是個結構體,結構體大小是內部全部大小的和,因此cache_t佔有16個字節


4.class_data_bits_t bits

bits是數據存放的地方,bits裏面有datadata是從macho裏面讀取的class_rw_t

class_rw_t *data() {
    return bits.data();
}複製代碼
struct class_rw_t {
...
    const class_ro_t *ro;
    method_array_t methods;//方法列表
    property_array_t properties;//屬性列表
    protocol_array_t protocols;//協議列表
...
}複製代碼

class_rw_t有方法列表,屬性列表,協議列表等等

class_rw_t裏面有個class_ro_t

struct class_ro_t {
    ..
    method_list_t * baseMethodList;//方法列表
    protocol_list_t * baseProtocols;//協議列表
    const ivar_list_t * ivars;//實例變量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;//屬性列表
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};複製代碼

class_ro_t也有方法列表,屬性列表,協議列表等,可是多了ivar_list_t實例變量列表。下面探索下二者等區別

4.類添加的屬性方法

類中添加實例變量sex,屬性age,對象方法work,類方法play

@interface CJPerson : NSObject{
    NSString * sex;
}
@property (nonatomic , copy , readonly) NSString * age;

- (void)work;
+ (void)play;複製代碼

object_getClass輸出下類對象

CJPerson * person = [CJPerson alloc];
Class cls = object_getClass(person);         
NSLog(@"%@",cls);複製代碼

x/4gx打印cls的內存地址以下:


經過類的數據結構分析,bits位於ISA,superclass,cache以後,這三者分別是8,8,16字節,根據內存偏移,bitsisa地址後的32字節,

0x100001218 + 32字節 = 0x100001238複製代碼

pop直接打印都不行,那麼須要用class_data_bits_t類型強轉打印


取出bits內的class_rw_t


class_rw_t的methods


看到methods中有3個方法,分別輸出下

  • .cxx_destruct 系統添加c++的析構方法

  • work 對象方法

  • age 屬性生成的age方法

  • method_t中的types,在蘋果開發者文檔中有詳細說明

結論:有對象方法work,竟然沒有類方法play

class_rw_t的properties

如法炮製,輸出下屬性列表properties


結論:符合預期,只有一個添加的age屬性

那繼續看下class_ro_t


class_ro_t的ivar_list_t


結論:ivar_list_t中有添加的實例變量sex,還有一個_age,這也符合常規認知,屬性在底層會生成帶下劃線的實例變量

class_ro_t的baseMethodList

結論:和class_rw_t中的methods一致

class_ro_t的baseProperties

結論:和class_rw_t中的properties一致

搜索疑點

目前添加的實例變量,屬性,對象方法都在內存中尋找到了,惟獨缺乏類方法,不過根據經驗能夠知道,類方法是緩存在元類中,那嘗試去元類搜索

經過isa_mask找到元類,而後一步步找到baseMethodList,果真在其中找到類方法play

結論

  • 成員變量存放在class_ro_t中的ivar_list_t
  • 屬性在class_rw_t中的property_array_tclass_ro_t中的的property_list_t都存着一份,而且會生成實例變量,和對應的方法
  • 方法在class_rw_t中的method_array_tclass_ro_t中的的method_list_t都存着一份
  • 對象方法存放在裏面
  • 類方法存放在元類裏面

class_ro_t和class_rw_t內容大部分相同的緣由:

class_ro_t存儲了當前類在編譯期就已經肯定的 屬性 方法 以及 遵循的協議, class_rw_t 是在運行時才肯定,它會先將 class_ro_t 的內容拷貝過去,而後再將當前類的分類的這些屬性、方法等拷貝到其中。 class_rw_t class_ro_t 的超集。


5.類的面試題

這是一道比較經典的面試題,CJPerson繼承NSObject

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //        
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //        
BOOL re3 = [(id)[CJPerson class] isKindOfClass:[CJPerson class]];       //        
BOOL re4 = [(id)[CJPerson class] isMemberOfClass:[CJPerson class]];     //        
NSLog(@"%hhd%hhd%hhd%hhd",re1,re2,re3,re4);      

  
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //        
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //        
BOOL re7 = [(id)[CJPerson alloc] isKindOfClass:[CJPerson class]];       //        
BOOL re8 = [(id)[CJPerson alloc] isMemberOfClass:[CJPerson class]];     //
NSLog(@"%hhd%hhd%hhd%hhd",re5,re6,re7,re8);複製代碼

輸出爲1000,1111

re1是NSObject調用類方法isKindOfClass

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}複製代碼

遞歸查找NSObject的元類是否存在,存在就繼續找元類的父類,直到根元類指向NSObject的時候,返回YES複製代碼

re2是NSObject調用類方法isMemberOfClass

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}複製代碼

只對比一次,NSObject的元類是否等於NSObject,返回NO複製代碼

re3是CJPerson調用類方法isKindOfClass

第一步就到CJPerson元類,而後一直遞歸下去,不會回到CJPerson類,返回NO複製代碼

re4是CJPerson調用類方法isMemberOfClass

只對比一次CJPerson元類和CJPerson類,返回NO複製代碼

re5是NSObject對象調用對象方法isKindOfClass

- (BOOL)isKindOfClass:(Class)cls { 
   for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}複製代碼

遞歸查找對象的父類是否等於NSObject,總有一次等於NSObject,返回YES複製代碼

re6是NSObject對象調用對象方法isMemberOfClass

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}複製代碼

只對比一次,NSObject對象的類是否等於NSObject,恰好繼承關係知足複製代碼

re7是CJPerson對象調用對象方法isKindOfClass

遞歸查找CJPerson對象的類是否等於CJPerson,第一次就知足繼承關係複製代碼

re8是CJPerson對象調用對象方法isMemberOfClass

只對比一次,CJPerson對象的類是否等於CJPerson,恰好繼承關係知足複製代碼


寫在最後

以上就是關於類的探索,下一章是cache_t流程分析引出消息發送流程,後續繼續更新類的底層結構,block,鎖,多線程等底層探索,還有應用程序加載,啓動優化,內存優化等相關知識點,敬請關注。

相關文章
相關標籤/搜索