《轉》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的細節ui

什麼是 SEL

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

typedef struct objc_selector *SEL;

SEL是一個指向objc_selector結構體的指針。而 objc_selector 的定義並無在runtime.h中給出定義。咱們能夠嘗試運行以下代碼:spa

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

輸出以下:ssr

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

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

  • name 是指 class_name 而不是 category_name
  • cls是要擴展的類對象,編譯期間是不會定義的,而是在Runtime階段經過name對應到對應的類對象
  • 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的知識:

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的過程:

1. [NSObject new]生成一個NSObject對象
2. 直接在該對象的類(NSObject)的方法列表裏找
3. 可以找到,因此正常輸出結果
相關文章
相關標籤/搜索