讀過《Runtime的初步認識——結構體與類》的小夥伴們應該對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_ivar_list *ivars
,
方法列表struct objc_method_list **methodLists
,
緩存方法列表struct objc_cache *cache
。
在這裏插一嘴。我們之前在《Runtime的初步認識——消息機制》中介紹過,在Objective-C裏面調用一個方法的時候,runtime層會將這個調用翻譯成
objc_msgSend(id self, SEL op, ...)
而objc_msgSend
具體有事如何分發的呢?我們來看下runtime層objc_msgSend
的源碼。(runtime的源代碼可以在 http://opensource.apple.com//tarballs/objc4/ 下載)
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd,...);
*
********************************************************************/
ENTRY _objc_msgSend
MESSENGER_START
CALL_MCOUNTER
// load receiver and selector
movl selector(%esp), %ecx
movl self(%esp), %eax
// check whether selector is ignored
cmpl $ kIgnore, %ecx
je LMsgSendDone // return self from %eax
// check whether receiver is nil
testl %eax, %eax
je LMsgSendNilSelf
// receiver (in %eax) is non-nil: search the cache
LMsgSendReceiverOk:
movl isa(%eax), %edx // class = self->isa
CacheLookup WORD_RETURN, MSG_SEND, LMsgSendCacheMiss
xor %edx, %edx // set nonstret for msgForward_internal
MESSENGER_END_FAST
jmp *%eax
// cache miss: go search the method lists
LMsgSendCacheMiss:
MethodTableLookup WORD_RETURN, MSG_SEND
xor %edx, %edx // set nonstret for msgForward_internal
jmp *%eax // goto *imp
// message sent to nil: redirect to nil receiver, if any
LMsgSendNilSelf:
// %eax is already zero
movl $0,%edx
xorps %xmm0, %xmm0
LMsgSendDone:
MESSENGER_END_NIL
ret
// guaranteed non-nil entry point (disabled for now)
// .globl _objc_msgSendNonNil
// _objc_msgSendNonNil:
// movl self(%esp), %eax
// jmp LMsgSendReceiverOk
LMsgSendExit:
END_ENTRY _objc_msgSend
這TMD什麼鬼??? 保證看完你是這個反應。。。Apple爲了高度優化這些方法的性能,這些方法都是彙編寫成的。不過雖然我們看不懂彙編。但是通過註釋我們也能瞭解消息分發的大概邏輯了。(這個地方我們可以先簡單瞭解到這,感興趣的可以繼續研究,一起分享交流)接下來我們切回正題。
當我們向對象發送消息的時候,OC會到緩存方法列表中開始找方法的指針,如果緩存列表中找不到,就會到方法列表中找,如果本類的方法列表中找不到,就會到父類裏面找,直到找到方法的指針或者最終的父類NSObject
也找不到方法的指針爲止。當找不到方法指針的時候,編譯器會發出[XXXX 某方法]unrecognized selector sent to instance 0x100400d90
的警告。
當找到方法指針的時候,OC會將會在內存中找到方法指針所指向的那個代碼塊,並運行它。
我們知道,程序之所以能運行,是因爲方法和變量都是存在程序的內存中。所以如果我們改變了方法指針指針所指向的內存地址的內容或者直接改變了方法指針指向的地址,我們就可以改變了方法的實現。
Runtime中的方法交換
Runtime給了我們一個函數來實現方法交換,你只需要導入objc/Runtime.h
文件即可使用這個函數。
這個函數是
/**
* Exchanges the implementations of two methods.
* 交換兩個方法的實現
*
* @param m1 Method to exchange with second method.
* @param m2 Method to exchange with first method.
*
* @note This is an atomic version of the following:
* 這個函數的實現如下:
* \code
* IMP imp1 = method_getImplementation(m1);
* IMP imp2 = method_getImplementation(m2);
* method_setImplementation(m1, imp2);
* method_setImplementation(m2, imp1);
* \endcode
*/
void method_exchangeImplementations(Method m1, Method m2);
這個方法的註釋官方已給出, 我們只要關注他的參數以及返回值(由於返回值爲void, 所以在此沒有多餘的解釋, 主要解釋兩個參數)。
Method
是Objective-C語言中的一個結構體, 在runtime.h
頭文件中有定義. 在這個函數中, Method
顧名思義就是要交換的方法. 我們可以通過下面這個函數來獲取一個類的Method
。
Method class_getInstanceMethod(Class cls, SEL name);
現在這兩個參數是我們平時看的見的參數。綜上所述,我們只要將兩組要交換的方法的SEL
和該方法所在的Class
傳入進去即可實現方法交換。
由此最終的代碼會變成:
Method m1 = class_getInstanceMethod([M1 class], @selector(method1name));
Method m2 = class_getInstanceMethod([M2 class], @selector(method2name));
method_exchangeImplementations(m1, m2);
如果你不知道方法交換的最終效果,現在我們用一個很簡單的例子來說明這個問題。
比如我們現在有兩個類的文件,每個類都有自己的方法和實現。
@interface classOne : NSObject
@end
@implementation classOne()
- (void)methodOne {
NSLog(@"methodOne");
}
@end
@interface classTwo : NSObject
@end
@implementation classTwo()
- (void)methodTwo {
NSLog(@"methodTwo");
}
@end
正常情況下
如果我們調用[[classOne new] methodOne]
則會輸出methodOne
。
同理如果調用[[classTwo new] methodTwo]
則會輸出methodTwo
。
但是如果我們在某一個時刻執行了一次
下面的代碼
Method method1 = class_getInstanceMethod([classTwo class], @selector(methodTwo));
Method method2 = class_getInstanceMethod([classOne class], @selector(methodOne));
method_exchangeImplementations(method1, method2);
在此之後(直到程序結束前),我們運行[[classOne new] methodOne]
的時候,打印的是methodTwo
。
這個就是runtime的黑科技,慎用~
這個就是runtime的黑科技,慎用~
這個就是runtime的黑科技,慎用~
通過以上內容, 你應該可以深刻體會到OC爲什麼是一個有運行時特色的語言了
還有個問題就是我們到底應該在什麼地方調用切換方法的代碼呢?
我們還要了解每個類都有個load
方法,這個方法是類加載到內存是調用的,所以我們可以在任意一個類的load
方法裏寫這個函數。