Runtime源碼淺析(內(nèi)部分享)

前段時間,公司內(nèi)部開發(fā)小組進行了一場Runtime分享交流會,我也重新拾遺了一些與Runtime相關(guān)的知識,現(xiàn)分享出來,一起學(xué)習(xí)。

1.準(zhǔn)備:

之前文章:
Runtime在工作中的運用

Runtime經(jīng)典面試題(附答案)

源碼:

Runtime開源代碼
編譯好的objc-750

關(guān)鍵詞:

OC對象、Class、isa指針


2.NSObject對象的Class

2.1 Class類型

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

在Runtime源碼中,我們能發(fā)現(xiàn)NSObject對象只有一個Class類型的成員變量:isa

typedef struct objc_class *Class;

Class對象其實是一個指向objc_class結(jié)構(gòu)體的指針。

struct objc_object {
private:
    isa_t isa;
// 這里省略成員變量以及方法...
}

Class類型本質(zhì)是個結(jié)構(gòu)體,該結(jié)構(gòu)體中存儲了該NSObject中的所有信息。

那么一個NSObject對象占用多少內(nèi)存?

NSObjcet實際上是只有一個名為isa的指針的結(jié)構(gòu)體,因此占用一個指針變量所占用的內(nèi)存空間大小,如果64bit(64位架構(gòu)中)占用8個字節(jié),如果32bit占用4個字節(jié)。

2.2 Class方法

- (Class)class {
    return object_getClass(self);
}

在Runtime源碼中,我們調(diào)用Class方法,其實是在調(diào)用object_getClass(self),最終通過下面代碼獲取結(jié)果值。

inline Class 
objc_object::ISA() 
{
   // 忽略其它方法
    return (Class)(isa.bits & ISA_MASK);
}

2.3 isa.bits & ISA_MASK

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
};

上述源碼可以知道,isa_t是個聯(lián)合體。

typedef unsigned long       uintptr_t;

bitslong類型的數(shù)值。

isa.h中,可以找到ISA_MASK源碼

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# else
#   error unknown architecture for packed isa
# endif

可知,其實ISA_MASK還是個數(shù)值類型

我們可以看到class方法最終獲取的即是:

結(jié)構(gòu)體objc_objectisa.bits & ISA_MASK的數(shù)值計算結(jié)果。


3.NSObject對象的isa_t

3.1 isa_t

// 精簡過的isa_t共用體
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
#endif
};

上述源碼中isa_t是union(共用體)類型??梢钥吹焦灿皿w中有一個結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)部分別定義了一些變量,變量后面的值代表的是該變量占用多少個二進制位,也就是位域技術(shù)。

源碼中通過共用體的形式存儲了64位的值,這些值在結(jié)構(gòu)體中被展示出來,通過對bits進行位運算而取出相應(yīng)位置的值。

3.2 共用體

在進行某些算法的C語言編程的時候,需要使幾種不同類型的變量存放到同一段內(nèi)存單元中。也就是使用覆蓋技術(shù),幾個變量互相覆蓋。這種幾個不同的變量共同占用一段內(nèi)存的結(jié)構(gòu),在C語言中,被稱作“共用體”類型結(jié)構(gòu),簡稱共用體,也叫聯(lián)合體。

優(yōu)點:可以很大程度上節(jié)省內(nèi)存空間。

union U1  
{  
    int n;  
    char s[11];  
    double d;  
};

對于U1共用體,s占11字節(jié),n占4字節(jié),d占8字節(jié),因此其至少需11字節(jié)的空間。然而其實際大小并不是11,用運算符sizeof測試其大小為16。這是因為內(nèi)存對齊原則,11既不能被4整除,也不能被8整除。因此補充字節(jié)到16,這樣就符合所有成員的自身對齊了。所以聯(lián)合體的內(nèi)存除了取最大成員內(nèi)存外,還要保證是所有成員類型size的最小公倍數(shù)

對比類的內(nèi)存對齊:

原則 1. 前面的地址必須是后面的地址正數(shù)倍,不是就補齊。
原則 2. 整個Struct的地址必須是最大字節(jié)的整數(shù)倍。

@interface MXRPerson : NSObject{
    int _age;
}

person對象的第一個地址要存放isa指針需要8個字節(jié),第二個地址要存放_age成員變量需要4個字節(jié),因此person對象就占用16個字節(jié)空間。

代碼驗證:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 驗證內(nèi)存地址
        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"%zd",class_getInstanceSize([NSObject class]));
        NSLog(@"%zd",class_getInstanceSize([MXRPerson class]));
    }
    return 0;
}
// 8  16

3.3 isa中存儲的信息及作用

struct {
    // 0代表普通的指針,存儲著Class,Meta-Class對象的內(nèi)存地址。
    // 1代表優(yōu)化后的使用位域存儲更多的信息。
    uintptr_t nonpointer        : 1; 

   // 是否有設(shè)置過關(guān)聯(lián)對象,如果沒有,釋放時會更快
    uintptr_t has_assoc         : 1;

    // 是否有C++析構(gòu)函數(shù),如果沒有,釋放時會更快
    uintptr_t has_cxx_dtor      : 1;

    // 存儲著Class、Meta-Class對象的內(nèi)存地址信息
    uintptr_t shiftcls          : 33; 

    // 用于在調(diào)試時分辨對象是否未完成初始化
    uintptr_t magic             : 6;

    // 是否有被弱引用指向過。
    uintptr_t weakly_referenced : 1;

    // 對象是否正在釋放
    uintptr_t deallocating      : 1;

    // 引用計數(shù)器是否過大無法存儲在isa中
    // 如果為1,那么引用計數(shù)會存儲在一個叫SideTable的類的屬性中
    uintptr_t has_sidetable_rc  : 1;

    // 里面存儲的值是引用計數(shù)器減1
    uintptr_t extra_rc          : 19;
};

此時我們重新來看ISA_MASK的值 0000000ffffffff8 轉(zhuǎn)為二進制:

111111111111111111111111111111111000

可以看出ISA_MASK的值轉(zhuǎn)化為二進制中有33位都為1,所以按位與的作用是可以取出這33位中的值。我們再回頭看看isa_t的源碼,不難發(fā)現(xiàn),這33位對應(yīng)的是結(jié)構(gòu)體的shiftcls的位域。那么ISA_MASKshiftcls進行按位與運算即可以取出Class或Meta-Class的值(內(nèi)存地址的值)。

同時可以看出ISA_MASK最后三位的值為0,那么任何數(shù)同ISA_MASK按位與運算之后,得到的最后三位必定都為0,因此任何類對象或元類對象的內(nèi)存地址最后三位必定為0,轉(zhuǎn)化為十六進制末位必定為8或者0。

對象的isa指針需要同ISA_MASK經(jīng)過一次&(按位與)運算才能得出真正的Class對象地址。

代碼驗證

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MXRPerson *person = [[MXRPerson alloc]init];
    NSLog(@"%p",[person class]);
    NSLog(@"%@",person);
}
2019-04-24 18:21:30.424630+0800 IsaTestDemo[58799:8221193] 0x1005c8db0
(lldb) p/x person->isa
(Class) $0 = 0x000001a1005c8db1 MXRPerson

shiftcls中存儲類對象地址。把轉(zhuǎn)為2進制的實例對象isa地址與轉(zhuǎn)為2進制的類對象地址作對比,可以看出存儲類對象地址的33位二進制內(nèi)容完全相同。

屏幕快照 2019-05-11 下午10.14.56.png

4.Class對象在內(nèi)存中存儲的信息

4.1 instance對象在內(nèi)存中存儲的信息包括

  1. isa指針
  2. 其他成員變量

4.2 class對象在內(nèi)存中存儲的信息主要包括

  1. isa指針
  2. superclass指針
  3. 類的屬性信息(@property),類的成員變量信息(ivar)
  4. 類的對象方法信息(instance method),類的協(xié)議信息(protocol)

4.3 class_rw_t & class_ro_t

我們發(fā)現(xiàn)class_rw_t中存儲著方法列表,屬性列表,協(xié)議列表等內(nèi)容。

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // 方法進行緩存
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }

class_rw_t中的methods是二維數(shù)組的結(jié)構(gòu),并且可讀可寫,因此可以動態(tài)的添加方法,并且更加便于分類方法的添加。

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;

而class_rw_t是通過bits調(diào)用data方法得來的,我們來到data方法內(nèi)部實現(xiàn)。我們可以看到,data函數(shù)內(nèi)部僅僅對bits進行&FAST_DATA_MASK操作

class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

成員變量信息則是存儲在class_ro_t內(nèi)部中的,我們來到class_ro_t內(nèi)查看。

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;
    }
};

4.4 每個類在內(nèi)存中有且只有一個meta-class對象,在內(nèi)存中存儲的信息主要包括

  1. isa指針
  2. superclass指針
  3. 類的類方法的信息(class method)

5.驗證對象的isa指針指向

1.當(dāng)對象調(diào)用實例方法的時候,我們上面講到,實例方法信息是存儲在class類對象中的,那么要想找到實例方法,就必須找到class類對象,那么此時isa的作用就來了

instance的isa指向class,當(dāng)調(diào)用對象方法時,通過instance的isa找到class,最后找到對象方法的實現(xiàn)進行調(diào)用。

2.當(dāng)類對象調(diào)用類方法的時候,同上,類方法是存儲在meta-class元類對象中的。那么要找到類方法,就需要找到meta-class元類對象,而class類對象的isa指針就指向元類對象

class的isa指向meta-class當(dāng)調(diào)用類方法時,通過class的isa找到meta-class,最后找到類方法的實現(xiàn)進行調(diào)用

3.當(dāng)對象調(diào)用其父類對象方法的時候,又是怎么找到父類對象方法的呢?,此時就需要使用到class類對象superclass指針。

當(dāng)Student的instance對象要調(diào)用Person的對象方法時,會先通過isa找到Student的class,然后通過superclass找到Person的class,最后找到對象方法的實現(xiàn)進行調(diào)用,同樣如果Person發(fā)現(xiàn)自己沒有響應(yīng)的對象方法,又會通過Person的superclass指針找到NSObject的class對象,去尋找響應(yīng)的方法

4.當(dāng)類對象調(diào)用父類的類方法時,就需要先通過isa指針找到meta-class,然后通過superclass去尋找響應(yīng)的方法

當(dāng)Student的class要調(diào)用Person的類方法時,會先通過isa找到Student的meta-class,然后通過superclass找到Person的meta-class,最后找到類方法的實現(xiàn)進行調(diào)用

經(jīng)典isa指向圖.png

代碼驗證:

struct mxr_objc_class{
    Class isa;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 如何證明isa指針的指向真的如上面所說?
        NSObject *object = [[NSObject alloc] init];
        Class objectClass = [NSObject class];
        // 我們自己創(chuàng)建一個同樣的結(jié)構(gòu)體并通過強制轉(zhuǎn)化拿到isa指針。
        struct mxr_objc_class *objectClass2 = (__bridge struct mxr_objc_class *)(objectClass);
        Class objectMetaClass = object_getClass([NSObject class]);
        NSLog(@"%p %p %p", object, objectClass, objectMetaClass);
    }
    return 0;
}

驗證結(jié)果1

 (lldb) p/x object->isa
 (Class) $0 = 0x001d800100b16141 NSObject
 (lldb) p/x objectClass
 (Class) $1 = 0x0000000100b16140 NSObject
 
 (lldb) p/x 0x00007ffffffffff8 & 0x001d800100b16141
 (long) $2 = 0x0000000100b16140

object-isa指針地址0x001dffff96537141經(jīng)過同0x00007ffffffffff8位運算,得出objectClass的地址0x00007fff96537140

驗證結(jié)果2

我們來驗證class對象的isa指針是否同樣需要位運算計算出meta-class對象的地址。
以同樣的方式打印objectClass->isa指針時,發(fā)現(xiàn)無法打印。

(lldb) p/x objectClass->isa
error: member reference base type 'Class' is not a structure or union

為了拿到isa指針的地址,我們自己創(chuàng)建一個同樣的結(jié)構(gòu)體并通過強制轉(zhuǎn)化拿到isa指針。

 (lldb) p/x objectClass2->isa
 (Class) $0 = 0x001d800100b160f1
 (lldb) p/x objectMetaClass
 (Class) $1 = 0x0000000100b160f0
 (lldb) p/x 0x00007ffffffffff8 & 0x001d800100b160f1
 (long) $2 = 0x0000000100b160f0

objectClass2的isa指針經(jīng)過位運算之后的地址是meta-class的地址。

參考文章

最新Runtime源碼objc4-750編譯
探尋OC對象的本質(zhì)
淺析NSObject對象的Class
神經(jīng)病院Objective-C Runtime入院第一天——isa和Class

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

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

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