你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 刨根問底Objective-C Runtime

刨根問底Objective-C Runtime

編輯:IOS開發基礎

前言

關於Objective-C Runtime一篇好的文檔 : Understanding the Objective-C Runtime

譯文地址為: http://blog.cocoabit.com/blog/2014/10/06/yi-li-jieobjective-cruntime/

Objective-C Runtime源碼是開源的,下載地址為: http://opensource.apple.com/tarballs/objc4/

習題內容

@唐巧_boy在微博上分享了他們技術討論會關於objc runtime的討論習題內容,習題來自 sunnyxx(博客)。以下是習題內容(圖片轉自@唐巧_boy微博):

01.jpg

自己做完這些題之後,也順便復習了一些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

本篇筆記主要是講述objc runtime中關於Object & Class & Meta Class的細節。

習題內容

下面代碼的運行結果是?

@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 指針。看下圖:

02.jpg

  • 每個Class都有一個isa指針指向一個唯一的Meta Class

  • 每一個Meta Class的isa指針都指向最上層的Meta Class(圖中的NSObject的Meta Class)

  • 最上層的Meta Class的isa指針指向自己,形成一個回路

  • 每一個Meta Class的super class指針指向它原本Class的 Super Class的Meta Class。但是最上層的Meta Class的 Super Class指向NSObject Class本身

  • 最上層的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")));

先看前兩個調用:

  • 最外層是 objc_msgSend函數,轉發消息。

  • 函數第一個參數是 (id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class"))

  • 函數第二個參數是轉發的selector

  • 函數第三個參數是 ((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的圖和實現,我們可以看出

  • 當 NSObject Class對象第一次進行比較時,得到它的isa為 NSObject的Meta Class, 這個時候 NSObject Meta Class 和 NSObject Class不相等。

  • 然後取NSObject 的Meta Class 的Super class,這個時候又變成了 NSObject Class, 所以返回相等。

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

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

- (BOOL)isMemberOf:aClass
{
    return isa == (Class)aClass;
}

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

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

繼續看後面兩個調用:

  • Sark Class 的isa指向的是 Sark的Meta Class,和Sark Class不相等

  • Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等

  • NSObject Meta Class的 super class 指向 NSObject Class,和 Sark Class 不相等

  • NSObject Class 的super class 指向 nil, 和 Sark Class不相等

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

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

本篇筆記主要是講述objc runtime的 消息和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. 先從當前class的cache方法列表(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;
};
  • mask: 指定分配cache buckets的總數。在方法查找中,Runtime使用這個字段確定數組的索引位置

  • occupied: 實際占用cache buckets的總數

  • 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,
};
  • _OBJC_$_CATEGORY_NSObject_$_Sark是按規則生成的字符串,我們可以清楚的看到是NSObject類,且Sark是NSObject類的Category

  • _category_t結構體第二項 classref_t 沒有數據,驗證了我們上面的說法

  • 由於題目中只有 - (void)foo方法,所以結構體中存儲的list只有第三項instanceMethods被填充。

  • _I_NSObject_Sark_foo代表了Category的foo方法,I表示實例方法

  • 最後這個類的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的知識:

  • objc_msgSend 第一個參數是  “(id)objc_getClass("NSObject")”,獲得NSObject Class的對象。

  • 類方法在Meta Class的方法列表中找,我們在load Category方法時加入的是- (void)foo實例方法,所以並不在NSOBject Meta Class的方法列表中

  • 繼續往 super class中找,在上一篇博客中我們知道,NSObject Meta Class的super class是NSObject本身。所以,這個時候我們能夠找到- (void)foo 這個方法。

  • 所以正常輸出結果

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

  • [NSObject new]生成一個NSObject對象。

  • 直接在該對象的類(NSObject)的方法列表裡找。

  • 能夠找到,所以正常輸出結果。

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

本篇筆記主要是講述objc runtime的 成員變量和屬性。

習題內容

下面代碼會? 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 。看下圖:

03.png

上圖中,左側的數據就是地址偏移字節,我們對 ivar 的訪問就可以通過 對象地址 + ivar偏移字節的方法。但是這又引發一個問題,看下圖:

04.png

我們增加了父類的ivar,這個時候布局就出錯了,我們就不得不重新編譯子類來恢復兼容性。

而Objective-C Runtime中使用了Non Fragile ivars,看下圖:

05.png

使用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語言到頭來講是一個順序運行的語言,隨著程序運行,棧中的地址依次往下走。

看下圖,可以清楚的展示整個計算的過程:

05.png

我們可以做一個另外的實驗,把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

關於C語言內存分配和使用的問題可參考這篇文章 http://www.th7.cn/Program/c/201212/114923.shtml。
(via:Chun Tips)

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved