前言
關於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微博):
自己做完這些題之後,也順便復習了一些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 指針。看下圖:
每個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消息函數,這個函數完成了動態綁定的所有事情。它的運行流程如下:
檢查selector是否需要忽略。(ps: Mac開發中開啟GC就會忽略retain,release方法。)
檢查target是否為nil。如果為nil,直接cleanup,然後return。(這就是我們可以向nil發送消息的原因。)
然後在target的Class中根據Selector去找IMP
尋找IMP的過程:
先從當前class的cache方法列表(cache methodLists)裡去找
找到了,跳到對應函數實現
沒找到,就從class的方法列表(methodLists)裡找
還找不到,就到super class的方法列表裡找,直到找到基類(NSObject)為止
最後再找不到,就會進入動態方法解析和消息轉發的機制。(這部分知識,下次再細談)
那麼什麼是方法列表呢?
上一篇博文中提到了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的知識:
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 。看下圖:
上圖中,左側的數據就是地址偏移字節,我們對 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
關於C語言內存分配和使用的問題可參考這篇文章 http://www.th7.cn/Program/c/201212/114923.shtml。
(via:Chun Tips)