PS 一些關於runtime的小demo在我的下一篇文章[iOS-Runtime-實踐篇](http://www.jianshu.com/p/d6a2656fc2cb)中
我們都知道Objective-C是一門動態語言,動態之處體現在它將許多靜態語言編譯連結時要做的事通通放到執行時去做,這大大增加了我們程式設計的靈活性。
毫不過分地說,Runtime就是OC的靈魂。接下來我就要撥開OC最外層的外衣,帶大家看看OC的真面目(C/C++)。
類和物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@interface Person : NSObject { NSString *_name; int _age; } - (void)study; + (void)study; @end @implementation Person - (void)study { NSLog(@"instance - study"); } + (void)study { NSLog(@"class - study"); } @end |
為了更好地說明類在底層的表現形式是怎樣, 我們將上面程式碼利用clang -rewrite-objc Person.m
指令將其用C/C++重寫, 一窺究竟.
把不必要的刪除, 整理後為下面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// class的結構 struct _class_t { struct _class_t *isa; // isa指標 struct _class_t *superclass; // 父類 void *cache; void *vtable; struct _class_ro_t *ro; // class的其他資訊 }; // class包含的資訊 struct _class_ro_t { unsigned int flags; unsigned int instanceStart; unsigned int instanceSize; unsigned int reserved; const unsigned char *ivarLayout; const char *name; // 類名 const struct _method_list_t *baseMethods; // 方法列表 const struct _objc_protocol_list *baseProtocols; // 協議列表 const struct _ivar_list_t *ivars; // ivar列表 const unsigned char *weakIvarLayout; const struct _prop_list_t *properties; // 屬性列表 }; // Person(class) struct _class_t OBJC_CLASS_$_Person = { .isa = &OBJC_METACLASS_$_Person, // 指向Person-metaclass .superclass = &OBJC_CLASS_$_NSObject, // 指向NSObject-class .cache = &_objc_empty_cache, 0, // unused, was (void *)&_objc_empty_vtable, &_OBJC_CLASS_RO_$_Person, // 包含了例項方法, ivar資訊等 }; // Person(metaclass) struct _class_t OBJC_METACLASS_$_Person = { .isa = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass .superclass = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass .cache = &_objc_empty_cache, 0, // unused, was (void *)&_objc_empty_vtable, &_OBJC_METACLASS_RO_$_Person, // 包含了類方法 }; |
原來(顯然), 我們的類其實就是一個結構體!!! 類跟我們的物件一樣, 都有一個isa指標, 所以類其實也是物件的一種.
isa指標
isa指標非常重要, 物件需要通過isa指標找到它的類, 類需要通過isa找到它的元類. 這在呼叫例項方法和類方法的時候起到重要的作用.
例項物件在呼叫方法時, 首先通過isa指標找到它所屬的類, 然後在類的快取(cache)裡找該方法的IMP, 如果沒有, 則去類的方法列表中查詢, 然後找到則呼叫該方法, 找不到則報錯.
類物件呼叫方法則如出一轍, 通過isa指標找到元類, 然後就跟上述一致了. 這裡涉及的傳送訊息機制下面會詳細講..
下面展示一些執行時動態獲取物件和類的屬性的C語言方法
類和類名 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 返回物件的類 Class object_getClass ( id obj ); // 設定物件的類 Class object_setClass ( id obj, Class cls ); // 獲取類的父類 Class class_getSuperclass ( Class cls ); // 建立一個新類和元類 Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes ); // 在應用中註冊由objc_allocateClassPair建立的類 void objc_registerClassPair ( Class cls ); // 銷燬一個類及其相關聯的類 void objc_disposeClassPair ( Class cls ); // 獲取類的類名 const char * class_getName ( Class cls ); // 返回給定物件的類名 const char * object_getClassName ( id obj ); |
ivar和屬性 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 新增成員變數 BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types ); // 新增屬性 BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount ); // 返回類的某一ivar Ivar class_getInstanceVariable(__unsafe_unretained Class cls, const char *name) // 返回物件中例項變數的值 id object_getIvar ( id obj, Ivar ivar ); // 設定物件中例項變數的值 void object_setIvar ( id obj, Ivar ivar, id value ); // 獲取整個成員變數列表 Ivar * class_copyIvarList ( Class cls, unsigned int *outCount ); // 獲取屬性列表 objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount ); |
方法 :
1 2 3 4 5 6 7 8 9 10 11 12 |
// 新增方法 BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types ); // 獲取例項方法 Method class_getInstanceMethod ( Class cls, SEL name ); // 獲取類方法 Method class_getClassMethod ( Class cls, SEL name ); // 獲取所有方法的陣列 Method * class_copyMethodList ( Class cls, unsigned int *outCount ); // 替代方法的實現 IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types ); // 交換兩個方法的實現(Method Swizzling) void method_exchangeImplementations(Method m1, Method m2) |
這裡說個注意點 : addIvar
並不能為一個已經存在的類新增成員變數, 只能為那些執行時動態新增的類, 並且只能在objc_allocateClassPair
與objc_registerClassPair
這兩個方法之間才能新增Ivar.
訊息傳送和轉發機制
在OC中, 如果向某物件傳送訊息, 那就會使用動態繫結機制來決定需要呼叫的方法. OC的方法在底層都是普通的C語言函式, 所以物件收到訊息後究竟要呼叫什麼函式完全由執行時決定, 甚至可以在執行時改變執行的方法.
[person read:book];
會被編譯成
objc_msgSend(person, @selector(read:), book);
objc_msgSend的具體流程如下
1. 通過isa指標找到所屬類
2. 查詢類的cache列表, 如果沒有則下一步
3. 查詢類的”方法列表”
4. 如果能找到與選擇子名稱相符的方法, 就跳至其實現程式碼
5. 找不到, 就沿著繼承體系繼續向上查詢
6. 如果能找到與選擇子名稱相符的方法, 就跳至其實現程式碼
7. 找不到, 執行”訊息轉發”.
訊息轉發
上面我們提到, 如果到最後都找不到, 就會來到訊息轉發
- 動態方法解析 : 先問接收者所屬的類, 你看能不能動態新增個方法來處理這個”未知的選擇子”? 如果能, 則訊息轉發結束.
- 備胎(後備接收者) : 請接收者看看有沒有其他物件能處理這條訊息? 如果有, 則把訊息轉給那個物件, 訊息轉發結束.
- 訊息簽名 : 這裡會要求你返回一個訊息簽名, 如果返回nil, 則訊息轉發結束.
- 完整的訊息轉發 : 備胎都搞不定了, 那就只能把該訊息相關的所有細節都封裝到一個NSInvocation物件, 再問接收者一次, 快想辦法把這個搞定了. 到了這個地步如果還無法處理, 訊息轉發機制也無能為力了.
動態方法解析 :
物件在收到無法解讀的訊息後, 首先呼叫其所屬類的這個類方法 :
1 2 3 4 |
+ (BOOL)resolveInstanceMethod:(SEL)selector // selector : 那個未知的選擇子 // 返回YES則結束訊息轉發 // 返回NO則進入備胎 |
假如尚未實現的方法不是例項方法而是類方法, 則會呼叫另一個方法resolveClassMethod:
備胎 :
動態方法解析失敗, 則呼叫這個方法
1 2 3 |
- (id)forwardingTargetForSelector:(SEL)selector // selector : 那個未知的選擇子 // 返回一個能響應該未知選擇子的備胎物件 |
通過備胎這個方法, 可以用”組合”來模擬出”多重繼承”.
訊息簽名 :
備胎搞不定, 這個方法就準備要被包裝成一個NSInvocation物件, 在這裡要先返回一個方法簽名
1 2 |
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector // NSMethodSignature : 該selector對應的方法簽名 |
完整的訊息轉發 :
給接收者最後一次機會把這個方法處理了, 搞不定就直接程式崩潰!
1 2 |
- (void)forwardInvocation:(NSInvocation *)invocation // invocation : 封裝了與那條尚未處理的訊息相關的所有細節的物件 |
在這裡能做的比較現實的事就是 : 在觸發訊息前, 先以某種方式改變訊息內容, 比如追加另外一個引數, 或是改變選擇子等等. 實現此方法時, 如果發現某呼叫操作不應該由本類處理, 可以呼叫超類的同名方法. 則繼承體系中的每個類都有機會處理該請求, 直到NSObject. 如果NSObject搞不定, 則還會呼叫doesNotRecognizeSelector:來丟擲異常, 此時你就會在控制檯看到那熟悉的unrecognized selector sent to instance..
上面這4個方法均是模板方法,開發者可以override,由runtime來呼叫。最常見的實現訊息轉發,就是重寫方法3和4,忽略這個訊息或者代理給其他物件.
Method Swizzling
被稱為黑魔法的一個方法, 可以把兩個方法的實現互換.
如上文所述, 類的方法列表會把選擇子的名稱對映到相關的方法實現上, 使得”動態訊息派發系統”能夠據此找到應該呼叫的方法. 這些方法均以函式指標的形式來表示, 這種指標叫做IMP,
1 |
id (*IMP)(id, SEL, ...) |
OC執行時系統提供了幾個方法能夠用來操作這張表, 動態增加, 刪除, 改變選擇子對應的方法實現, 甚至交換兩個選擇子所對映到的指標. 如,
如何交換兩個已經寫好的方法實現?
1 2 3 4 |
// 取得方法 Method class_getInstanceMethod(Class aClass, SEL aSelector) // 交換實現 void method_exchangeImplementations(Method m1, Method m2) |
通過Method Swizzling可以為一些完全不知道其具體實現的黑盒方法增加日誌記錄功能, 利於我們除錯程式. 並且我們可以將某些系統類的具體實現換成我們自己寫的方法, 以達到某些目的. (例如, 修改主題, 修改字型等等)
KVO原理
KVO的實現也依賴Runtime. Apple文件曾簡單提到過KVO的實現原理 :
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …
Apple的文件提得不多, 但是大神Mike Ash在很早很早以前就已經做過研究, 摘下了KVO神祕的面紗了, 有興趣的可以去查下, 這裡不多深究, 只是簡單闡述下原理.
原來當你對一個物件進行觀察時, 系統會自動新建一個類繼承自原類, 然後重寫被觀察屬性的setter
方法. 然後重寫的setter
方法會負責在呼叫原setter
方法前後通知觀察者. 然後把原物件的isa指標指向這個新類, 我們知道, 物件是通過isa指標去查詢自己是屬於哪個類, 並去所在類的方法列表中查詢方法的, 所以這個時候這個物件就自然地變成了新類的例項物件.
不僅如此, Apple還重寫了原類的- class
方法, 檢視欺騙我們, 這個類沒有變, 還是原來的那個類. 只要我們懂得Runtime的原理, 這一切都只是掩耳盜鈴罷了.
後記
這只是我的Runtime文章的第一篇, 之後還會有Runtime實踐篇以及利用Runtime解決實際問題的幾個demo, 感興趣的話還請大家關注關注^_^
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式