iOS探索 類的結構分析

我是好寶寶發表於2020-01-21

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)

寫在前面

iOS探索系列前面幾篇講到了物件的初始化、物件的記憶體分佈、物件的isa初始化以及指向分析,本文就來講講例項出例項物件的類物件——類

一、類的本質

1.類的本質

objc原始碼下準備程式碼

#import <objc/runtime.h>

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *p = [FXPerson alloc];
        Class cls = object_getClass(p);
    }
    return 0;
}
複製程式碼

利用clang將OC檔案輸出cpp檔案(彷彿開啟了新世界的大門)

iOS探索 類的結構分析
發現類在底層用Class接收

然後開始大海撈針(開天眼模式)找到了Class的定義

typedef struct objc_class *Class;
複製程式碼

想要找到objc_class就搜不到了,但是總覺得它似曾相似,或許能在objc原始碼找到靈感

在原始碼中搜尋程式碼的經驗 "objc_class :"、 "objc_class {"

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_class繼承於objc_object

struct objc_object {
private:
    isa_t isa;
    
public:
    ...
}
複製程式碼

結論:類的本質是objc_class型別的結構體,objc_class繼承於objc_object,所以滿足萬物皆物件

以後不要再在面試的時候回答一句 萬物皆物件 就完事了

2.objc_object和NSObject的關係

等等,為什麼繼承objc_object就滿足萬物皆物件了???

看過NSObject的定義就知道了

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製程式碼

仔細比較的話就能看出NSObjectobjc_object有著說不清道不明的關係

  • 其實NSObjectobjc_object的仿寫,和objc_object的定義是一樣的,在底層會編譯成objc_object
  • 同理NSObject類是OC版本的objc_class

3.Class isa??

isa明明是isa_t型別的,為什麼註釋了一句Class ISA

  • 萬物皆物件,用繼承於objc_object的Class接收是沒問題的
  • 強轉,方便isa走位時返回類的型別

二、類的結構

objc_class的定義可以得出,類有4個屬性:isa、superclass、cache、bits

1.Class ISA

不但例項物件中有isa指標類物件中也有isa指標關聯著元類

Class本身就是一個指標,佔用8位元組

2.Class superclass

顧名思義就是類的父類(一般為NSObject)superclass是Class型別,所以佔用8位元組

3.cache_t cache

雖然對這個屬性比較陌生(詳見iOS探索 cache_t分析),但是cache在英文中的意思是快取

cache_t是一個結構體,記憶體長度有所有元素決定:_buckets是一個指標,佔用8位元組;mask_t是個int型別,_mask佔用4位元組;_occupied佔用4位元組

=>cache_t佔用16位元組

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

public:
    ...
};
複製程式碼

4.class_data_bits_t bits

又是一個陌生的屬性,但是蘋果工程師還是蠻友好的,這一看就是存資料的地方

struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;
private:
    bool getBit(uintptr_t bit)
    {
        return bits & bit;
    }
    ...
public:

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ...
};
複製程式碼

那麼問題來了,類的屬性方法都去哪兒了?是在cache還是在bits? 其實前文中有提到一丟丟——objc_class中有個class_rw_t *data()方法

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    ...
}
複製程式碼

三、類的屬性方法

為FxPerson新增nickname成員屬性、nationality屬性變數、walk類方法、fly例項方法

@interface FXPerson : NSObject {
    NSString *nickname;
}
@property (nonatomic, copy) NSString *nationality;
+ (void)walk;
- (void)fly;
@end

@implementation FXPerson
+ (void)walk {}
- (void)fly {}
@end
複製程式碼

1.類的屬性

x/4gx cls列印當前類結構

iOS探索 類的結構分析

bits剛好是類的記憶體首地址+isa、superclass、cache的記憶體長度

=> 0x100001560+32位元組 = 0x100001580

po列印不出來,那就型別強轉列印輸出bits的記憶體地址

iOS探索 類的結構分析

根據class_rw_t *data() { return bits.data(); }列印bits.data()

iOS探索 類的結構分析

再往裡面探索進去(這比西天取經還難)

iOS探索 類的結構分析

天吶!怎麼只有一個nationality了?難道我FXPerson類只配擁有“國籍”不配擁有“姓名”?讓我靜靜... 既然此路不通,那麼換條路走走(內心很不情願)

在一頓猛如虎的操作(開天眼)之後,發現了class_rw_t有個屬性class_ro_t

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    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;
    }
};
複製程式碼

清空控制檯輸出ro,跟class_ro_t的結構型別一摸一樣

iOS探索 類的結構分析

列印robaseProperties

iOS探索 類的結構分析

天吶?這個成員變數nickname是迷路了嗎?等等!可能是自己迷路了!

嘗試了一下列印ivars找到了nickname,但是為什麼count = 2呢?

iOS探索 類的結構分析

ivar_list_t繼承entsize_list_tt,而後者又有個get(uint32_t i)方法

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

struct entsize_list_tt {
    ...
    Element& get(uint32_t i) const { 
        assert(i < count);
        return getOrEnd(i);
    }
    ...
}
複製程式碼

如願拿到了nicknamenationality,但是這個nationality長得有點不一樣

iOS探索 類的結構分析

仔細想想這不就是編譯器會在底層自動將屬性變數生成一個成員變數_nationality(_字首+屬性變數)

2.類的方法

列印robaseMethodList

iOS探索 類的結構分析

系統在底層新增了一個c++的.cxx_destruct方法,同時編譯器還在底層幫屬性變數生成了一個setter方法getter方法

但是FXPerson的walk類方法又被吃了...

無奈之下只能開大招(開天眼)在元類的datarobaseMethodList找到了

iOS探索 類的結構分析

其實也很好理解:類方法可以理解成元類物件例項方法,因此存在元類

3.結論

  • 成員變數存放在ivar
  • 屬性存放在property,同時也會存一份在ivar,並生成settergetter方法
  • 物件方法存放在裡面
  • 類方法存放在元類裡面

4.API驗證

利用底層開放的API可以驗證以上結論

iOS探索 類的結構分析

void testObjc_copyIvar_copyProperies(Class cls) {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(cls, &count);
    for (unsigned int i=0; i < count; i++) {
        Ivar const ivar = ivars[i];
        //獲取例項變數名
        const char*cName = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:cName];
        NSLog(@"class_copyIvarList:%@",ivarName);
    }
    free(ivars);

    unsigned int pCount = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &pCount);
    for (unsigned int i=0; i < pCount; i++) {
        objc_property_t const property = properties[i];
        //獲取屬性名
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        //獲取屬性值
        NSLog(@"class_copyProperiesList:%@",propertyName);
    }
    free(properties);
}

void testObjc_copyMethodList(Class cls) {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //獲取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        NSLog(@"Method, name: %@", key);
    }
    free(methods);
}

void testInstanceMethod_classToMetaclass(Class cls) {
    const char *className = class_getName(cls);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(cls, @selector(walk));
    Method method2 = class_getInstanceMethod(metaClass, @selector(fly));

    Method method3 = class_getInstanceMethod(cls, @selector(walk));
    Method method4 = class_getInstanceMethod(metaClass, @selector(fly));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

void testClassMethod_classToMetaclass(Class cls) {
    const char *className = class_getName(cls);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(cls, @selector(walk));
    Method method2 = class_getClassMethod(metaClass, @selector(fly));

    Method method3 = class_getClassMethod(cls, @selector(walk));
    Method method4 = class_getClassMethod(metaClass, @selector(fly));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

void testIMP_classToMetaclass(Class cls) {
    const char *className = class_getName(cls);
    Class metaClass = objc_getMetaClass(className);

    IMP imp1 = class_getMethodImplementation(cls, @selector(walk));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(fly));

    IMP imp3 = class_getMethodImplementation(cls, @selector(walk));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(fly));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
    NSLog(@"%s",__func__);
}
複製程式碼

5.clang編譯驗證

其實直接用clang編譯也能看出蹊蹺

iOS探索 類的結構分析

關於v@:這個玩意,在蘋果開發者文件上也有介紹

四、寫在後面

雖然一直碰壁,但是本文最後還是找到了想要的答案,有的時候你離成功只差一步之遙,也有的時候是你選錯了研究方向。學習是如此,人生亦是如此,道阻且長,且行且珍惜。幸運的是有前人替我們踩坑,我們可以吸取前人的教訓少走彎路

相關文章