Objective-C Runtime的知識

筆記分爲四篇:

刨根問底Objective-C Runtime(1)- Self & Super

刨根問底Objective-C Runtime(2)- Object & Class & Meta Class

刨根問底Objective-C Runtime(3)- 消息和Category

刨根問底Objective-C Runtime(4)- 成員變量與屬性

刨根問底Objective-C Runtime(1)- Self & Super

下面的代碼輸出什麼?

@implementation Son : Father

- (id)init

{

    self = [super init];

    if (self)

    {

        NSLog(@"%@", NSStringFromClass([self class]));

        NSLog(@"%@", NSStringFromClass([super class]));

    }

    return self;

}

@end

答案:都輸出 Son

2014-11-05 11:06:18.060 Test[8566:568584] NSStringFromClass([self class]) = Son

2014-11-05 11:06:18.061 Test[8566:568584] NSStringFromClass([super class]) = Son

解惑:這個題目主要是考察關於objc中對 self 和 super 的理解。

self 是類的隱藏參數,指向當前調用方法的這個類的實例。而 super 是一個 Magic Keyword, 它本質是一個編譯器標示符,和 self 是指向的同一個消息接受者。上面的例子不管調用[self class]還是[super class],接受消息的對象都是當前 Son *xxx 這個對象。而不同的是,super是告訴編譯器,調用 class 這個方法時,要去父類的方法,而不是本類裏的。

當使用 self 調用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;而當使用 super 時,則從父類的方法列表中開始找。然後調用父類的這個方法。

真的是這樣嗎?繼續看:

使用clang重寫命令:

$ clang -rewrite-objc test.m

發現上述代碼被轉化爲:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));

從上面的代碼中,我們可以發現在調用 [self class] 時,會轉化成 objc_msgSend函數。看下函數定義:

id objc_msgSend(id self, SEL op, ...)

我們把 self 做爲第一個參數傳遞進去。

而在調用 [super class]時,會轉化成 objc_msgSendSuper函數。看下函數定義:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一個參數是 objc_super 這樣一個結構體,其定義如下:

struct objc_super {

   __unsafe_unretained id receiver;

   __unsafe_unretained Class super_class;

};

結構體有兩個成員,第一個成員是 receiver, 類似於上面的 objc_msgSend函數第一個參數self 。第二個成員是記錄當前類的父類是什麼。

所以,當調用 [self class] 時,實際先調用的是 objc_msgSend函數,第一個參數是 Son當前的這個實例,然後在 Son 這個類裏面去找 - (Class)class這個方法,沒有,去父類 Father裏找,也沒有,最後在 NSObject類中發現這個方法。而 - (Class)class的實現就是返回self的類別,故上述輸出結果爲 Son。

objc Runtime開源代碼對- (Class)class方法的實現:

- (Class)class {

    return object_getClass(self);

}

而當調用 [super class]時,會轉換成objc_msgSendSuper函數。第一步先構造 objc_super 結構體,結構體第一個成員就是 self 。第二個成員是 (id)class_getSuperclass(objc_getClass(“Son”)) , 實際該函數輸出結果爲 Father。第二步是去 Father這個類裏去找- (Class)class,沒有,然後去NSObject類去找,找到了。最後內部是使用 objc_msgSend(objc_super->receiver, @selector(class))去調用,此時已經和[self class]調用相同了,故上述輸出結果仍然返回 Son。

刨根問底Objective-C Runtime(2)- Object & Class & Meta Clas

下面代碼的運行結果是?

@interface Sark : NSObject

@end

@implementation Sark

@end

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];

        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];

        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];

        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

        NSLog(@"%d %d %d %d", res1, res2, res3, res4);

    }

    return 0;

}

運行結果爲:

2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0

這裏先看幾個概念

什麼是 id

id 在 objc.h 中定義如下:

/// A pointer to an instance of a class.

typedef struct objc_object *id;

就像註釋中所說的這樣 id 是指向一個 objc_object 結構體的指針。

id 這個struct的定義本身就帶了一個 , 所以我們在使用其他NSObject類型的實例時需要在前面加上 , 而使用 id 時卻不用。
那麼objc_object又是什麼呢

objc_object 在 objc.h 中定義如下:

/// Represents an instance of a class.

struct objc_object {

    Class isa;

};

這個時候我們知道Objective-C中的object在最後會被轉換成C的結構體,而在這個struct中有一個 isa 指針,指向它的類別 Class。

那麼什麼是Class呢

在 objc.h 中定義如下:
/// An opaque type that represents an Objective-C class.

typedef struct objc_class *Class;
我們可以看到 Class本身指向的也是一個C的struct objc_class。

繼續看在runtime.h中objc_class定義如下:

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;

    #if !__OBJC2__

    Class super_class                                        OBJC2_UNAVAILABLE;

    const char *name                                         OBJC2_UNAVAILABLE;

    long version                                             OBJC2_UNAVAILABLE;

    long info                                                OBJC2_UNAVAILABLE;

    long instance_size                                       OBJC2_UNAVAILABLE;

    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;

    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;

    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;

    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

    #endif

} OBJC2_UNAVAILABLE;

該結構體中,isa 指向所屬Class, super_class指向父類別。

繼續看

下載objc源代碼,在 objc-runtime-new.h 中,我們發現 objc_class有如下定義:

struct objc_class : objc_object {

    // Class ISA;

    Class superclass;   

    ...

    ...

}

豁然開朗,我們看到在Objective-C的設計哲學中,一切都是對象。Class在設計中本身也是一個對象。而這個Class對象的對應的類,我們叫它 Meta Class。即Class結構體中的 isa 指向的就是它的 Meta Class。

Meta Class

根據上面的描述,我們可以把Meta Class理解爲 一個Class對象的Class。簡單的說:

當我們發送一個消息給一個NSObject對象時,這條消息會在對象的類的方法列表裏查找

當我們發送一個消息給一個類時,這條消息會在類的Meta Class的方法列表裏查找

而 Meta Class本身也是一個Class,它跟其他Class一樣也有自己的 isa 和 super_class 指針。看下圖:
這裏寫圖片描述
1.每個Class都有一個isa指針指向一個唯一的Meta Class
2.每一個Meta Class的isa指針都指向最上層的Meta Class(圖中的NSObject的Meta Class)
3.最上層的Meta Class的isa指針指向自己,形成一個迴路
4.每一個Meta Class的super class指針指向它原本Class的 Super Class的Meta Class。但是最上層的Meta Class的 Super Class指向NSObject Class本身
5.最上層的NSObject Class的super class指向 nil
解惑

爲了更加清楚的知道整個函數調用過程,我們使用clang -rewrite-objc main.m重寫,可獲得如下代碼:

BOOL res1 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isKindOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));

BOOL res2 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));

BOOL res3 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Sark"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));

BOOL res4 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Sark"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));

先看前兩個調用:

1.最外層是 objc_msgSend函數,轉發消息。
2.函數第一個參數是 (id)((Class ()(id, SEL))(void )objc_msgSend)((id)objc_getClass(“NSObject”), sel_registerName(“class”))
3.函數第二個參數是轉發的selector
4.函數第三個參數是 ((Class ()(id, SEL))(void )objc_msgSend)((id)objc_getClass(“NSObject”), sel_registerName(“class”))

我們注意到第一個參數和第三個參數對應重寫的是[NSObject class],即使用objc_msgSend向 NSObject Class 發送 @selector(class) 這個消息

打開objc源代碼,在 Object.mm 中發現+ (Class)class實現如下:

+ (Class)class {

    return self;

}

所以即返回Class類的對象本身。看如下輸出:

NSLog(@"%p", [NSObject class]);

NSLog(@"%p", [NSObject class]);

2014-11-05 18:48:30.939 Test[11682:865988] 0x7fff768d40f0

2014-11-05 18:48:30.940 Test[11682:865988] 0x7fff768d40f0

繼續打開objc源代碼,在 Object.mm 中,我們發現 isKindOfClass的實現如下:

- (BOOL)isKindOf:aClass

{

    Class cls;

    for (cls = isa; cls; cls = cls->superclass) 

        if (cls == (Class)aClass)

            return YES;

    return NO;

}

對着上面Meta Class的圖和實現,我們可以看出

  1. 當 NSObject Class對象第一次進行比較時,得到它的isa爲 NSObject的Meta Class, 這個時候
    NSObject Meta Class 和 NSObject Class不相等。
  2. 然後取NSObject 的Meta Class 的Super class,這個時候又變成了 NSObject Class,所以返回相等。

    所以上述第一個輸出結果是 YES 。

我們在看下 ‘isMemberOfClass’的實現:

- (BOOL)isMemberOf:aClass

{

    return isa == (Class)aClass;

}

綜上所述,當前的 isa 指向 NSObject 的 Meta Class, 所以和 NSObject Class不相等。

所以上述第二個輸出結果爲 NO 。

繼續看後面兩個調用:


 1. Sark Class 的isa指向的是 Sark的Meta Class,和Sark Class不相等
 2. Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark
    Class不相等
 3. NSObject Meta Class的 super class 指向 NSObject Class,和 Sark Class 不相等
 4. NSObject Class 的super class 指向 nil, 和 Sark Class不相等

所以後面兩個調用的結果都輸出爲 NO 。

刨根問底Objective-C Runtime(3)- 消息 和 Category

下面的代碼會?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)

+ (void)foo;

@end

@implementation NSObject (Sark)

- (void)foo

{

    NSLog(@"IMP: -[NSObject(Sark) foo]");

}

@end

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        [NSObject foo];

        [[NSObject new] foo];

    }

    return 0;

}

答案:代碼正常輸出,輸出結果如下:

2014-11-06 13:11:46.694 Test[14872:1110786] IMP: -[NSObject(Sark) foo]

2014-11-06 13:11:46.695 Test[14872:1110786] IMP: -[NSObject(Sark) foo]

使用clang -rewrite-objc main.m重寫,我們可以發現 main 函數中兩個方法調用被轉換成如下代碼:

 ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("foo"));

 ((void (*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")), sel_registerName("foo"));

我們發現上述兩個方法最終轉換成使用 objc_msgSend 函數傳遞消息。

這裏先看幾個概念

objc_msgSend函數定義如下:

id objc_msgSend(id self, SEL op, ...)

關於 id 的解釋請看objc runtime系列第二篇博文: objc runtime中Object & Class & Meta Class的細節

什麼是 SEL

打開objc.h文件,看下SEL的定義如下:

typedef struct objc_selector *SEL;

SEL是一個指向objc_selector結構體的指針。而 objc_selector 的定義並沒有在runtime.h中給出定義。我們可以嘗試運行如下代碼:

SEL sel = @selector(foo);

NSLog(@"%s", (char *)sel);

NSLog(@"%p", sel);

const char *selName = [@"foo" UTF8String];

SEL sel2 = sel_registerName(selName);

NSLog(@"%s", (char *)sel2);

NSLog(@"%p", sel2);

輸出如下:

2014-11-06 13:46:08.058 Test[15053:1132268] foo

2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114

2014-11-06 13:46:08.058 Test[15053:1132268] foo

2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114

Objective-C在編譯時,會根據方法的名字生成一個用來區分這個方法的唯一的一個ID。只要方法名稱相同,那麼它們的ID就是相同的。

兩個類之間,不管它們是父類與子類的關係,還是之間沒有這種關係,只要方法名相同,那麼它的SEL就是一樣的。每一個方法都對應着一個SEL。編譯器會根據每個方法的方法名爲那個方法生成唯一的SEL。這些SEL組成了一個Set集合,當我們在這個集合中查找某個方法時,只需要去找這個方法對應的SEL即可。而SEL本質是一個字符串,所以直接比較它們的地址即可。

當然,不同的類可以擁有相同的selector。不同類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。

那麼什麼是IMP呢

繼續看定義:

typedef id (*IMP)(id, SEL, ...);

IMP本質就是一個函數指針,這個被指向的函數包含一個接收消息的對象id,調用方法的SEL,以及一些方法參數,並返回一個id。因此我們可以通過SEL獲得它所對應的IMP,在取得了函數指針之後,也就意味着我們取得了需要執行方法的代碼入口,這樣我們就可以像普通的C語言函數調用一樣使用這個函數指針。

那麼 objc_msgSend 到底是怎麼工作的呢?

在Objective-C中,消息直到運行時纔會綁定到方法的實現上。編譯器會把代碼中[target doSth]轉換成 objc_msgSend消息函數,這個函數完成了動態綁定的所有事情。它的運行流程如下:


 1. 檢查selector是否需要忽略。(ps: Mac開發中開啓GC就會忽略retain,release方法。)
 2. 檢查target是否爲nil。如果爲nil,直接cleanup,然後return。(這就是我們可以向nil發送消息的原因。)
 3. 然後在target的Class中根據Selector去找IMP

尋找IMP的過程:


 1. 先從當前classcache方法列表(cache methodLists)裏去找
 2. 找到了,跳到對應函數實現
 3. 沒找到,就從class的方法列表(methodLists)裏找
 4. 還找不到,就到super class的方法列表裏找,直到找到基類(NSObject)爲止
 5. 最後再找不到,就會進入動態方法解析和消息轉發的機制。

那麼什麼是方法列表呢?

上一篇博文中提到了objc_class結構體定義,如下:

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;

    #if !__OBJC2__

    Class super_class                                        OBJC2_UNAVAILABLE;

    const char *name                                         OBJC2_UNAVAILABLE;

    long version                                             OBJC2_UNAVAILABLE;

    long info                                                OBJC2_UNAVAILABLE;

    long instance_size                                       OBJC2_UNAVAILABLE;

    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;

    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;

    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;

    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

    #endif

} OBJC2_UNAVAILABLE;

struct objc_method_list {

    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;

}

1) objc_method_list 就是用來存儲當前類的方法鏈表,objc_method存儲了類的某個方法的信息。

Method

typedef struct objc_method *Method;

Method 是用來代表類中某個方法的類型,它實際就指向objc_method結構體,如下:

struct objc_method {

    SEL method_name                                          OBJC2_UNAVAILABLE;

    char *method_types                                       OBJC2_UNAVAILABLE;

    IMP method_imp                                           OBJC2_UNAVAILABLE;

}                                                            OBJC2_UNAVAILABLE;

method_types是個char指針,存儲着方法的參數類型和返回值類型。

SEL 和 IMP 就是我們上文提到的,所以我們可以理解爲objc_class中 method list保存了一組SEL<->IMP的映射。

2)objc_cache 用來緩存用過的方法,提高性能。

Cache

typedef struct objc_cache *Cache                           OBJC2_UNAVAILABLE;

實際指向objc_cache結構體,如下:

struct objc_cache {

    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;

    unsigned int occupied                                    OBJC2_UNAVAILABLE;

    Method buckets[1]                                        OBJC2_UNAVAILABLE;

};
  1. mask: 指定分配cache buckets的總數。在方法查找中,Runtime使用這個字段確定數組的索引位置
  2. occupied: 實際佔用cache buckets的總數
  3. buckets:
    指定Method數據結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被佔用,另外被佔用的bucket可能是不連續的。這個數組可能會隨着時間而增長。
    objc_msgSend每調用一次方法後,就會把該方法緩存到cache列表中,下次的時候,就直接優先從cache列表中尋找,如果cache沒有,才從methodLists中查找方法。

說完了 objc_msgSend, 那麼題目中的Category又是怎麼工作的呢?

繼續看概念

我們知道Catagory可以動態地爲已經存在的類添加新的方法。這樣可以保證類的原始設計規模較小,功能增加時再逐步擴展。在runtime.h中查看定義:

typedef struct objc_category *Category;

同樣也是指向一個 objc_category 的C 結構體,定義如下:

struct objc_category {

    char *category_name                                      OBJC2_UNAVAILABLE;

    char *class_name                                         OBJC2_UNAVAILABLE;

    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;

    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;

    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

}                                                            OBJC2_UNAVAILABLE;

通過上面的結構體,大家可以很清楚的看出存儲的內容。我們繼續往下看,打開objc源代碼,在 objc-runtime-new.h中我們可以發現如下定義:

struct category_t {

    const char *name;

    classref_t cls;

    struct method_list_t *instanceMethods;

    struct method_list_t *classMethods;

    struct protocol_list_t *protocols;

    struct property_list_t *instanceProperties;

};

上面的定義需要提到的地方有三點:


 1. name 是指 class_name 而不是 category_name
 2. cls是要擴展的類對象,編譯期間是不會定義的,而是在Runtime階段通過name對應到對應的類對象
 3. instanceProperties表示Category裏所有的properties,這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加實例變量的原因,不過這個和一般的實例變量是不一樣的

爲了驗證上述內容,我們使用clang -rewrite-objc main.m重寫,題目中的Category被編譯器轉換成了這樣:

// @interface NSObject (Sark)

// + (void)foo;

/* @end */

// @implementation NSObject (Sark)

static void _I_NSObject_Sark_foo(NSObject * self, SEL _cmd) {

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_dd1ee3_mi_0);

}

// @end

static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = 

{

    "NSObject",

    0, // &OBJC_CLASS_$_NSObject,

    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Sark,

    0,

    0,

    0,

};

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {

    &_OBJC_$_CATEGORY_NSObject_$_Sark,

};
  1. OBJC_CATEGORY_NSObject_ _Sark是按規則生成的字符串,我們可以清楚的看到是NSObject類,且Sark是NSObject類的Category
  2. _category_t結構體第二項 classref_t 沒有數據,驗證了我們上面的說法
  3. 由於題目中只有 - (void)foo方法,所以結構體中存儲的list只有第三項instanceMethods被填充。
  4. _I_NSObject_Sark_foo代表了Category的foo方法,I表示實例方法
  5. 最後這個類的Category生成了一個數組,存在了_objc_catlist裏,目前數組的內容只有一個&_OBJC_CATEGORY_NSObject_ _Sark
    最終這些Category裏面的方法是如何被加載的呢?

1.打開objc源代碼,找到 objc-os.mm, 函數_objc_init爲runtime的加載入口,由libSystem調用,進行初始化操作。

2.之後調用objc-runtime-new.mm -> map_images加載map到內存

3.之後調用objc-runtime-new.mm->_read_images初始化內存中的map, 這個時候將會load所有的類,協議還有Category。NSOBject的+load方法就是這個時候調用的

這裏貼上Category被加載的代碼:

// Discover categories. 

for (EACH_HEADER) {

    category_t **catlist = 

        _getObjc2CategoryList(hi, &count);

    for (i = 0; i < count; i++) {

        category_t *cat = catlist[i];

        Class cls = remapClass(cat->cls);

        if (!cls) {

            // Category's target class is missing (probably weak-linked).

            // Disavow any knowledge of this category.

            catlist[i] = nil;

            if (PrintConnecting) {

                _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "

                             "missing weak-linked target class", 

                             cat->name, cat);

            }

            continue;

        }

        // Process this category. 

        // First, register the category with its target class. 

        // Then, rebuild the class's method lists (etc) if 

        // the class is realized. 

        BOOL classExists = NO;

        if (cat->instanceMethods ||  cat->protocols  

            ||  cat->instanceProperties) 

        {

            addUnattachedCategoryForClass(cat, cls, hi);

            if (cls->isRealized()) {

                remethodizeClass(cls);

                classExists = YES;

            }

            if (PrintConnecting) {

                _objc_inform("CLASS: found category -%s(%s) %s", 

                             cls->nameForLogging(), cat->name, 

                             classExists ? "on existing class" : "");

            }

        }

        if (cat->classMethods  ||  cat->protocols  

            /* ||  cat->classProperties */) 

        {

            addUnattachedCategoryForClass(cat, cls->ISA(), hi);

            if (cls->ISA()->isRealized()) {

                remethodizeClass(cls->ISA());

            }

            if (PrintConnecting) {

                _objc_inform("CLASS: found category +%s(%s)", 

                             cls->nameForLogging(), cat->name);

            }

        }

    }

}

1) 循環調用了 _getObjc2CategoryList方法,這個方法的實現是:

GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");

方法中最後一個參數__objc_catlist就是編譯器剛剛生成的category數組

2) load完所有的categories之後,開始對Category進行處理。

從上面的代碼中我們可以發現:實例方法被加入到了當前的類對象中, 類方法被加入到了當前類的Meta Class中 (cls->ISA)

Step 1. 調用addUnattachedCategoryForClass方法

Step 2. 調用remethodizeClass方法, 在remethodizeClass的實現裏調用attachCategoryMethods

static void 

attachCategoryMethods(Class cls, category_list *cats, bool flushCaches)

{

    if (!cats) return;

    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    method_list_t **mlists = (method_list_t **)

        _malloc_internal(cats->count * sizeof(*mlists));

    // Count backwards through cats to get newest categories first

    int mcount = 0;

    int i = cats->count;

    BOOL fromBundle = NO;

    while (i--) {

        method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);

        if (mlist) {

            mlists[mcount++] = mlist;

            fromBundle |= cats->list[i].fromBundle;

        }

    }

    attachMethodLists(cls, mlists, mcount, NO, fromBundle, flushCaches);

    _free_internal(mlists);

}

這裏把一個類的category_list的所有方法取出來生成了method list。這裏是倒序添加的,也就是說,新生成的category的方法會先於舊的category的方法插入。

之後調用attachMethodLists將所有方法前序添加進類的method list中,如果原來類的方法列表是a,b,Category的方法列表是c,d。那麼插入之後的方法列表將會是c,d,a,b。

小發現

看上面被編譯器轉換的代碼,我們發現Category頭文件被註釋掉了,結合上面category的加載過程。這就是我們即使沒有import category的頭文件,都能夠成功調用到Category方法的原因。

runtime加載完成後,Category的原始信息在類結構中將不會存在。

解惑

根據上面提到的知識,我們對題目中的代碼進行分析。

1) objc runtime加載完後,NSObject的Sark Category被加載。而NSObject的Sark Category的頭文件 + (void)foo 並沒有實質參與到工作中,只是給編譯器進行靜態檢查,所有我們編譯上述代碼會出現警告,提示我們沒有實現 + (void)foo 方法。而在代碼編譯中,它已經被註釋掉了。

2) 實際被加入到Class的method list的方法是 - (void)foo,它是一個實例方法,所以加入到當前類對象NSObject的方法列表中,而不是NSObject Meta class的方法列表中。

3) 當執行 [NSObject foo]時,我們看下整個objc_msgSend的過程:

結合上一篇Meta Class的知識:


 1. objc_msgSend 第一個參數是  “(id)objc_getClass("NSObject")”,獲得NSObject
    Class的對象。
 2. 類方法在Meta Class的方法列表中找,我們在load Category方法時加入的是-
    (void)foo實例方法,所以並不在NSOBject Meta Class的方法列表中
 3. 繼續往 super class中找,在上一篇博客中我們知道,NSObject Meta Class的super
    class是NSObject本身。所以,這個時候我們能夠找到- (void)foo 這個方法。
 4. 所以正常輸出結果

4) 當執行[[NSObject new] foo],我們看下整個objc_msgSend的過程:

[NSObject new]生成一個NSObject對象。
直接在該對象的類(NSObject)的方法列表裏找。
能夠找到,所以正常輸出結果。

刨根問底Objective-C Runtime(4)- 成員變量與屬性

下面代碼會? Compile Error / Runtime Crash / NSLog…?

@interface Sark : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Sark

- (void)speak

{

    NSLog(@"my name is %@", self.name);

}

@end

@interface Test : NSObject

@end

@implementation Test

- (instancetype)init

{

    self = [super init];

    if (self) {

        id cls = [Sark class];

        void *obj = &cls;

        [(__bridge id)obj speak];

    }

    return self;

}

@end

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        [[Test alloc] init];

    }

    return 0;

}

答案:代碼正常輸出,輸出結果爲:

2014-11-07 14:08:25.698 Test[1097:57255] my name is

爲什麼呢?

前幾節博文中多次講到了objc_class結構體,今天我們再拿出來看一下:

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__

    Class super_class                                        OBJC2_UNAVAILABLE;

    const char *name                                         OBJC2_UNAVAILABLE;

    long version                                             OBJC2_UNAVAILABLE;

    long info                                                OBJC2_UNAVAILABLE;

    long instance_size                                       OBJC2_UNAVAILABLE;

    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;

    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;

    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;

    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;

其中objc_ivar_list結構體存儲着objc_ivar數組列表,而objc_ivar結構體存儲了類的單個成員變量的信息。

那麼什麼是Ivar呢?

Ivar 在objc中被定義爲:

typedef struct objc_ivar *Ivar;

它是一個指向objc_ivar結構體的指針,結構體有如下定義:

struct objc_ivar {

    char *ivar_name                                          OBJC2_UNAVAILABLE;

    char *ivar_type                                          OBJC2_UNAVAILABLE;

    int ivar_offset                                          OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

}                                                            OBJC2_UNAVAILABLE;

這裏我們注意第三個成員 ivar_offset。它表示基地址偏移字節。

在編譯我們的類時,編譯器生成了一個 ivar佈局,顯示了在類中從哪可以訪問我們的 ivars 。看下圖:
這裏寫圖片描述
上圖中,左側的數據就是地址偏移字節,我們對 ivar 的訪問就可以通過 對象地址 + ivar偏移字節的方法。但是這又引發一個問題,看下圖:
這裏寫圖片描述
我們增加了父類的ivar,這個時候佈局就出錯了,我們就不得不重新編譯子類來恢復兼容性。

而Objective-C Runtime中使用了Non Fragile ivars,看下圖:
這裏寫圖片描述
使用Non Fragile ivars時,Runtime會進行檢測來調整類中新增的ivar的偏移量。 這樣我們就可以通過 對象地址 + 基類大小 + ivar偏移字節的方法來計算出ivar相應的地址,並訪問到相應的ivar。

我們來看一個例子:

@interface Student : NSObject

{

    @private

    NSInteger age;

}

@end

@implementation Student

- (NSString *)description

{

    return [NSString stringWithFormat:@"age = %d", age];

}

@end

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        Student *student = [[Student alloc] init];

        student->age = 24;

    }

    return 0;

}

上述代碼,Student有兩個被標記爲private的ivar,這個時候當我們使用 -> 訪問時,編譯器會報錯。那麼我們如何設置一個被標記爲private的ivar的值呢?

通過上面的描述,我們知道ivar是通過計算字節偏量來確定地址,並訪問的。我們可以改成這樣:

@interface Student : NSObject

{

    @private

    int age;

}

@end

@implementation Student

- (NSString *)description

{

    NSLog(@"current pointer = %p", self);

    NSLog(@"age pointer = %p", &age);

    return [NSString stringWithFormat:@"age = %d", age];

}

@end

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        Student *student = [[Student alloc] init];

        Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");

        int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));

        NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));

        *age_pointer = 10;

        NSLog(@"%@", student);

    }

    return 0;

}

上述代碼的輸出結果爲:

2014-11-08 18:24:38.892 Test[4143:466864] age ivar offset = 8

2014-11-08 18:24:38.893 Test[4143:466864] current pointer = 0x1001002d0

2014-11-08 18:24:38.893 Test[4143:466864] age pointer = 0x1001002d8

2014-11-08 18:24:38.894 Test[4143:466864] age = 10

我們可以清晰的看到指針地址的變化和偏移量,和我們上述描述一致。

說完了Ivar, 那Property又是怎麼樣的呢?

使用clang -rewrite-objc main.m重寫題目中的代碼,我們發現Sark類中的name屬性被轉換成了如下代碼:

struct Sark_IMPL {

    struct NSObject_IMPL NSObject_IVARS;

    NSString *_name;

};

// @property (nonatomic, copy) NSString *name;

/* @end */

// @implementation Sark

static NSString * _I_Sark_name(Sark * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Sark$_name)); }

static void _I_Sark_setName_(Sark * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Sark, _name), (id)name, 0, 1); }

類中的Property屬性被編譯器轉換成了Ivar,並且自動添加了我們熟悉的Set和Get方法。

我們這個時候回頭看一下objc_class結構體中的內容,並沒有發現用來專門記錄Property的list。我們翻開objc源代碼,在objc-runtime-new.h中,發現最終還是會通過在class_ro_t結構體中使用property_list_t存儲對應的propertyies。

而在剛剛重寫的代碼中,我們可以找到這個property_list_t:

static struct /*_prop_list_t*/ {

    unsigned int entsize;  // sizeof(struct _prop_t)

    unsigned int count_of_properties;

    struct _prop_t prop_list[1];

    } _OBJC_$_PROP_LIST_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = {

        sizeof(_prop_t),

        1,

        name

};

static struct _class_ro_t _OBJC_CLASS_RO_$_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = {

    0, __OFFSETOFIVAR__(struct Sark, _name), sizeof(struct Sark_IMPL), 

    (unsigned int)0, 

    0, 

    "Sark",

    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Sark,

    0, 

    (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Sark,

    0, 

    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Sark,

};

解惑

1)爲什麼能夠正常運行,並調用到speak方法?

id cls = [Sark class];

void *obj = &cls;

[(__bridge id)obj speak];

obj被轉換成了一個指向Sark Class的指針,然後使用id轉換成了objc_object類型。這個時候的obj已經相當於一個Sark的實例對象(但是和使用[Sark new]生成的對象還是不一樣的),我們回想下Runtime的第二篇博文中objc_object結構體的構成就是一個指向Class的isa指針。

這個時候我們再回想下上一篇博文中objc_msgSend的工作流程,在代碼中的obj指向的Sark Class中能夠找到speak方法,所以代碼能夠正常運行。

2) 爲什麼self.name的輸出爲?

我們在測試代碼中加入一些調試代碼和Log如下:

- (void)speak

{ 

    unsigned int numberOfIvars = 0;

    Ivar *ivars = class_copyIvarList([self class], &numberOfIvars);

    for(const Ivar *p = ivars; p < ivars+numberOfIvars; p++) {

        Ivar const ivar = *p;

        ptrdiff_t offset = ivar_getOffset(ivar);

        const char *name = ivar_getName(ivar);

        NSLog(@"Sark ivar name = %s, offset = %td", name, offset);

    }

    NSLog(@"my name is %p", &_name);

    NSLog(@"my name is %@", *(&_name));

}

@implementation Test

- (instancetype)init

{

    self = [super init];

    if (self) {

        NSLog(@"Test instance = %@", self);

        void *self2 = (__bridge void *)self;

        NSLog(@"Test instance pointer = %p", &self2);

        id cls = [Sark class];

        NSLog(@"Class instance address = %p", cls);

        void *obj = &cls;

        NSLog(@"Void *obj = %@", obj);

        [(__bridge id)obj speak];

    }

    return self;

}

@end

輸出結果如下:

2014-11-11 00:56:02.464 Test[10475:1071029] Test instance = 2014-11-11 00:56:02.464 Test[10475:1071029] Test instance pointer = 0x7fff5fbff7c8

2014-11-11 00:56:02.465 Test[10475:1071029] Class instance address = 0x1000023c8

2014-11-11 00:56:02.465 Test[10475:1071029] Void *obj = 2014-11-11 00:56:02.465 Test[10475:1071029] Sark ivar name = _name, offset = 8

2014-11-11 00:56:02.465 Test[10475:1071029] my name is 0x7fff5fbff7c8

2014-11-11 00:56:02.465 Test[10475:1071029] my name is

Sark中Propertyname最終被轉換成了Ivar加入到了類的結構中,Runtime通過計算成員變量的地址偏移來尋找最終Ivar的地址,我們通過上述輸出結果,可以看到 Sark的對象指針地址加上Ivar的偏移量之後剛好指向的是Test對象指針地址。

這裏的原因主要是因爲在C中,局部變量是存儲到內存的棧區,程序運行時棧的生長規律是從地址高到地址低。C語言到頭來講是一個順序運行的語言,隨着程序運行,棧中的地址依次往下走。

看下圖,可以清楚的展示整個計算的過程:
這裏寫圖片描述
我們可以做一個另外的實驗,把Test Class 的init方法改爲如下代碼:

@interface Father : NSObject

@end

@implementation Father

@end

@implementation Test

- (instancetype)init

{

    self = [super init];

    if (self) {

        NSLog(@"Test instance = %@", self);

        id fatherCls = [Father class];

        void *father;

        father = (void *)&fatherCls;

        id cls = [Sark class];

        void *obj;

        obj = (void *)&cls;

        [(__bridge id)obj speak];

    }

    return self;

}

@end

你會發現這個時候的輸出變成了:

2014-11-08 21:40:36.724 Test[4845:543231] Test instance = 2014-11-08 21:40:36.725 Test[4845:543231] ivar name = _name, offset = 8

2014-11-08 21:40:36.726 Test[4845:543231] Sark instance = 0x7fff5fbff7b8

2014-11-08 21:40:36.726 Test[4845:543231] my name is 0x7fff5fbff7c0

2014-11-08 21:40:36.726 Test[4845:543231] my name is
發佈了45 篇原創文章 · 獲贊 7 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章