下面的代碼會?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
打開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。
繼續看定義:
typedef id (*IMP)(id, SEL, ...);
IMP本質就是一個函數指針,這個被指向的函數包含一個接收消息的對象id,調用方法的SEL,以及一些方法參數,並返回一個id。所以咱們能夠經過SEL得到它所對應的IMP,在取得了函數指針以後,也就意味着咱們取得了須要執行方法的代碼入口,這樣咱們就能夠像普通的C語言函數調用同樣使用這個函數指針。
在Objective-C中,消息直到運行時纔會綁定到方法的實現上。編譯器會把代碼中[target doSth]
轉換成 objc_msgSend
消息函數,這個函數完成了動態綁定的全部事情。它的運行流程以下:
尋找IMP的過程:
上一篇博文中提到了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
存儲了類的某個方法的信息。
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指針,存儲着方法的參數類型和返回值類型。2)objc_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; };
objc_msgSend
每調用一次方法後,就會把該方法緩存到cache
列表中,下次的時候,就直接優先從cache列表中尋找,若是cache沒有,才從methodLists中查找方法。
咱們知道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; };
上面的定義須要提到的地方有三點:
爲了驗證上述內容,咱們使用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表示實例方法__objc_catlist
裏,目前數組的內容只有一個&_OBJC_$_CATEGORY_NSObject_$_Sark
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. 可以找到,因此正常輸出結果