iOS runtime系列三 -- Method Swizzling

Method Swizzling參考資料

1.用到的運(yùn)行時(shí)基礎(chǔ)知識(shí)介紹

  • SEL : 方法選擇器,SEL是函數(shù)objc_msgSend第二個(gè)參數(shù)的數(shù)據(jù)類型,表示方法選擇器 ;其實(shí)它就是映射到方法的C字符串,你可以通過(guò)Objc編譯器命令@selector()/NSSelectorFromString()或者Runtime系統(tǒng)的sel_registerName函數(shù)來(lái)獲取一個(gè)SEL類型的方法選擇器
  • Method : 就是一個(gè)指向objc_method結(jié)構(gòu)體指針,它存儲(chǔ)了方法名(method_name)、方法類型(method_types)和一個(gè)指向方法實(shí)現(xiàn)的函數(shù)指針(method_imp)等信息
  • IMP : IMP本質(zhì)上就是一個(gè)函數(shù)指針,指向方法的實(shí)現(xiàn),當(dāng)你向某個(gè)對(duì)象發(fā)送一條信息,可以由這個(gè)函數(shù)指針來(lái)指定方法的實(shí)現(xiàn),它最終就會(huì)執(zhí)行那段代碼,這樣可以繞開消息傳遞階段而去執(zhí)行另一個(gè)方法實(shí)現(xiàn)
  • class_getInstanceMethod(Class cls, SEL name),獲取指定類中,方法選擇器對(duì)應(yīng)的方法,需要注意在搜索方法的時(shí)候,會(huì)先從指定類的方法列表中搜索,如果搜索不到(當(dāng)前類沒(méi)有實(shí)現(xiàn)此方法),就會(huì)去superclasses 中搜索
/** 
 * @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
 */
OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
  • OBJC_EXPORT IMP method_getImplementation(Method m)
    獲取某個(gè)方法對(duì)應(yīng)的方法實(shí)現(xiàn)
  • OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 為該類添加一個(gè)方法,但不會(huì)去重寫已有實(shí)現(xiàn)的方法,如果已經(jīng)有相同方法名稱則會(huì)返回NO;注意:父類中有對(duì)應(yīng)的方法而當(dāng)前類沒(méi)有,會(huì)添加成功
/** 
 * Adds a new method to a class with a given name and implementation.
 * 
 * @param cls The class to which to add a method.
 * @param name A selector that specifies the name of the method being added.
 * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
 * @param types An array of characters that describe the types of the arguments to the method. 
 * 
 * @return YES if the method was added successfully, otherwise NO 
 *  (for example, the class already contains a method implementation with that name).
 *
 * @note class_addMethod will add an override of a superclass's implementation, 
 *  but will not replace an existing implementation in this class. 
 *  To change an existing implementation, use method_setImplementation.
 */
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                                 const char *types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
  • OBJC_EXPORT IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) ,替換某個(gè)類中方法選擇器對(duì)應(yīng)方法的方法實(shí)現(xiàn)
/** 
 * Replaces the implementation of a method for a given class.
 * 
 * @param cls The class you want to modify.
 * @param name A selector that identifies the method whose implementation you want to replace.
 * @param imp The new implementation for the method identified by name for the class identified by cls.
 * @param types An array of characters that describe the types of the arguments to the method. 
 *  Since the function must take at least two arguments—self and _cmd, the second and third characters
 *  must be “@:” (the first character is the return type).
 * 
 * @return The previous implementation of the method identified by \e name for the class identified by \e cls.
 * 
 * @note This function behaves in two different ways:
 *  - If the method identified by \e name does not yet exist, it is added as if \c class_addMethod were called. 
 *    The type encoding specified by \e types is used as given.
 *  - If the method identified by \e name does exist, its \c IMP is replaced as if \c method_setImplementation were called.
 *    The type encoding specified by \e types is ignored.
 */
OBJC_EXPORT IMP class_replaceMethod(Class cls, SEL name, IMP imp, 
                                    const char *types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
  • method_exchangeImplementations(Method m1, Method m2) 交換兩個(gè)方法的方法實(shí)現(xiàn)
/** 
 * 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
 */
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

2. 方法交換

2.1 寫法一:有缺陷版本,不建議使用

+ (void)load
{
        Class class = [self class];
        SEL originalSelector = @selector(testButtonLog);
        SEL swizzledSelector = @selector(xl_testButtonLog);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);    
}

2.2 寫法二: 建議寫法,交換前用class_addMethod進(jìn)行一層判斷處理

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(testButtonLog);
        SEL swizzledSelector = @selector(xl_testButtonLog);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

2.3 比較兩種寫法差異

  • dispatch_once:保證代碼塊只會(huì)被執(zhí)行一次并且線程安全,不過(guò)load方法中并不需要,并不會(huì)被多次調(diào)用
  • 為什么需要用到class_addMethod
  • 我們想要對(duì)一個(gè)類的某個(gè)方法進(jìn)行擴(kuò)展,應(yīng)該把這種擴(kuò)展限制在當(dāng)前類和當(dāng)前類的子類中,不應(yīng)該影響到父類中的方法,因?yàn)檫@個(gè)父類可能還有其他派生類,修改了父類的方法可能造成一連串非預(yù)期的結(jié)果
  • 異常場(chǎng)景 : 假設(shè)我們要擴(kuò)展的方法(testButtonLog)是繼承自父類(TestButton)的,且當(dāng)前類沒(méi)有對(duì)此方法(testButtonLog)進(jìn)行重寫,按照寫法一實(shí)現(xiàn)方法交換,最終實(shí)現(xiàn)的結(jié)果是把父類(TestButton)中的方法(testButtonLog)和方法(xl_testButtonLog)進(jìn)行了交換,這樣造成的后果如下:
  • 如果是當(dāng)前類(或當(dāng)前類的子類)的實(shí)例對(duì)象調(diào)用 testButtonLog時(shí),運(yùn)行時(shí)會(huì)去執(zhí)行 xl_testButtonLog , 沒(méi)有什么問(wèn)題
  • 如果是當(dāng)前類的父類的實(shí)例對(duì)象調(diào)用testButtonLog時(shí),會(huì)有什么后果? 會(huì)調(diào)用當(dāng)前類的xl_testButtonLog方法,如果我們?cè)趚l_testButtonLog方法中這樣寫
```
- (void)xl_testButtonLog

{
NSLog(@"%s",func);
[self xl_testButtonLog]; // 調(diào)用此方法不會(huì)造成遞歸調(diào)用,因?yàn)樯厦孀隽朔椒ń粨Q,調(diào)用此方法,在運(yùn)行時(shí)會(huì)去執(zhí)行testButtonLog
}
```
會(huì)閃退,報(bào)錯(cuò)xl_testButtonLog方法找不到,原因:當(dāng)前類的父類中并沒(méi)有xl_testButtonLog方法具體可以看MethodSwizzlingDemo中情景2.2示例

閃退代碼示例

  • 使用class_addMethod后,如果當(dāng)前類沒(méi)有實(shí)現(xiàn)要擴(kuò)展的方法(testButtonLog), class_addMethod會(huì)自動(dòng)在當(dāng)前類中添加一個(gè)方法(testButtonLog),這樣交換后的結(jié)果就是當(dāng)前類中的方法(testButtonLog)和方法(xl_testButtonLog)進(jìn)行交換,不影響父類testButtonLog的正常調(diào)用
  • 具體效果演示,可以參考demoMethodSwizzlingDemo中三種情景的演示

3. 擴(kuò)展工具類

  • 對(duì)于Method Swizzling的實(shí)現(xiàn)方法可以抽取出來(lái),封裝到NSObject的分類中,方便以后使用,具體看demoMethodSwizzlingDemo中的NSObject+MethodSwizzling類
  • 交換兩個(gè)實(shí)例方法
/**
交換兩個(gè)實(shí)例方法
*/
+ (void)xl_exchangeInstanceMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
   Class class = [self class]; // 這個(gè)地方要注意
   Method originalMethod = class_getInstanceMethod(class, originalSelector);
   Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
   
   BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
   
   if (didAddMethod) {
       class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
   } else {
       method_exchangeImplementations(originalMethod, swizzledMethod);
   }
}
  • 使用示例:

    image.png

  • 交換兩個(gè)類方法

/**
交換兩個(gè)類方法
*/
+ (void)xl_exchangeClassMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
   Class class = object_getClass((id)self); // 這個(gè)地方要注意  
   Method originalMethod = class_getClassMethod(class, originalSelector);
   Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
   
   BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
   if (didAddMethod) {
       class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
   } else {
       method_exchangeImplementations(originalMethod, swizzledMethod);
   }
}
  • 注意點(diǎn):交換類方法時(shí),獲取類要用object_getClass((id)self)方法獲取class, 注意和通過(guò)[self class]獲取的class是不同的對(duì)象,可以通過(guò)打印hash值來(lái)看
Class class = object_getClass((id)self); // 這個(gè)地方要注意
Class class2 = [self class];

 NSLog(@" class = %@ , class hash = %zd",class,[class hash]);
 NSLog(@" class2 = %@ , class2 hash = %zd",class2,[class2 hash]);
    //2017-05-17 18:38:00.171 EHGhostDrone3[26572:677703]  class = NSObject , class hash =  4663643704
    //2017-05-17 18:38:00.171 EHGhostDrone3[26572:677703]  class2 = NSObject , class2 hash = 4663643784

4. Method Swizzling的弊端

Method Swizzling就像一把瑞士小刀,如果使用得當(dāng),它會(huì)有效地解決問(wèn)題。但使用不當(dāng),將帶來(lái)很多麻煩。在stackoverflow上有人已經(jīng)提出這樣一個(gè)問(wèn)題:What are the Dangers of Method Swizzling in Objective C?,它的危險(xiǎn)性主要體現(xiàn)以下幾個(gè)方面:

Method swizzling is not atomic
Changes behavior of un-owned code
Possible naming conflicts
Swizzling changes the method's arguments
The order of swizzles matters
Difficult to understand (looks recursive)
Difficult to debug

5. Method Swizzling 項(xiàng)目實(shí)踐應(yīng)用

5.1 自動(dòng)處理按鈕高亮和不可點(diǎn)擊狀態(tài)的屬性

5.2 自動(dòng)采集控制器的進(jìn)入,退出和銷毀等動(dòng)作

5.3 自動(dòng)采集按鈕的點(diǎn)擊事件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,774評(píng)論 0 9
  • 我們常常會(huì)聽說(shuō) Objective-C 是一門動(dòng)態(tài)語(yǔ)言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,231評(píng)論 0 7
  • Objective-C語(yǔ)言是一門動(dòng)態(tài)語(yǔ)言,它將很多靜態(tài)語(yǔ)言在編譯和鏈接時(shí)期做的事放到了運(yùn)行時(shí)來(lái)處理。這種動(dòng)態(tài)語(yǔ)言的...
    有一種再見叫青春閱讀 610評(píng)論 0 3
  • Objective-C語(yǔ)言是一門動(dòng)態(tài)語(yǔ)言,他將很多靜態(tài)語(yǔ)言在編譯和鏈接時(shí)期做的事情放到了運(yùn)行時(shí)來(lái)處理。這種動(dòng)態(tài)語(yǔ)言...
    tigger丨閱讀 1,431評(píng)論 0 8
  • 原文出處:南峰子的技術(shù)博客 Objective-C語(yǔ)言是一門動(dòng)態(tài)語(yǔ)言,它將很多靜態(tài)語(yǔ)言在編譯和鏈接時(shí)期做的事放到了...
    _燴面_閱讀 1,258評(píng)論 1 5