Runtime的實踐——方法交換

讀過《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方法裏寫這個函數。

相關推薦:
《Runtime的實踐——給一個類添加屬性(關聯對象)》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章