iOS runtime系列三 -- Method Swizzling

Method Swizzling參考資料

1.用到的運行時基礎知識介紹

  • SEL : 方法選擇器,SEL是函數objc_msgSend第二個參數的數據類型,表示方法選擇器 ;其實它就是映射到方法的C字符串,你可以通過Objc編譯器命令@selector()/NSSelectorFromString()或者Runtime系統的sel_registerName函數來獲取一個SEL類型的方法選擇器
  • Method : 就是一個指向objc_method結構體指針,它存儲了方法名(method_name)、方法類型(method_types)和一個指向方法實現的函數指針(method_imp)等信息
  • IMP : IMP本質上就是一個函數指針,指向方法的實現,當你向某個對象發送一條信息,可以由這個函數指針來指定方法的實現,它最終就會執行那段代碼,這樣可以繞開消息傳遞階段而去執行另一個方法實現
  • class_getInstanceMethod(Class cls, SEL name),獲取指定類中,方法選擇器對應的方法,需要注意在搜索方法的時候,會先從指定類的方法列表中搜索,如果搜索不到(當前類沒有實現此方法),就會去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)
    獲取某個方法對應的方法實現
  • OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 為該類添加一個方法,但不會去重寫已有實現的方法,如果已經有相同方法名稱則會返回NO;注意:父類中有對應的方法而當前類沒有,會添加成功
/** 
 * 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) ,替換某個類中方法選擇器對應方法的方法實現
/** 
 * 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) 交換兩個方法的方法實現
/** 
 * 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進行一層判斷處理

+ (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:保證代碼塊只會被執行一次并且線程安全,不過load方法中并不需要,并不會被多次調用
  • 為什么需要用到class_addMethod
  • 我們想要對一個類的某個方法進行擴展,應該把這種擴展限制在當前類和當前類的子類中,不應該影響到父類中的方法,因為這個父類可能還有其他派生類,修改了父類的方法可能造成一連串非預期的結果
  • 異常場景 : 假設我們要擴展的方法(testButtonLog)是繼承自父類(TestButton)的,且當前類沒有對此方法(testButtonLog)進行重寫,按照寫法一實現方法交換,最終實現的結果是把父類(TestButton)中的方法(testButtonLog)和方法(xl_testButtonLog)進行了交換,這樣造成的后果如下:
  • 如果是當前類(或當前類的子類)的實例對象調用 testButtonLog時,運行時會去執行 xl_testButtonLog , 沒有什么問題
  • 如果是當前類的父類的實例對象調用testButtonLog時,會有什么后果? 會調用當前類的xl_testButtonLog方法,如果我們在xl_testButtonLog方法中這樣寫
```
- (void)xl_testButtonLog

{
NSLog(@"%s",func);
[self xl_testButtonLog]; // 調用此方法不會造成遞歸調用,因為上面做了方法交換,調用此方法,在運行時會去執行testButtonLog
}
```
會閃退,報錯xl_testButtonLog方法找不到,原因:當前類的父類中并沒有xl_testButtonLog方法具體可以看MethodSwizzlingDemo中情景2.2示例

閃退代碼示例

  • 使用class_addMethod后,如果當前類沒有實現要擴展的方法(testButtonLog), class_addMethod會自動在當前類中添加一個方法(testButtonLog),這樣交換后的結果就是當前類中的方法(testButtonLog)和方法(xl_testButtonLog)進行交換,不影響父類testButtonLog的正常調用
  • 具體效果演示,可以參考demoMethodSwizzlingDemo中三種情景的演示

3. 擴展工具類

  • 對于Method Swizzling的實現方法可以抽取出來,封裝到NSObject的分類中,方便以后使用,具體看demoMethodSwizzlingDemo中的NSObject+MethodSwizzling類
  • 交換兩個實例方法
/**
交換兩個實例方法
*/
+ (void)xl_exchangeInstanceMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
   Class class = [self class]; // 這個地方要注意
   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

  • 交換兩個類方法

/**
交換兩個類方法
*/
+ (void)xl_exchangeClassMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
   Class class = object_getClass((id)self); // 這個地方要注意  
   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);
   }
}
  • 注意點:交換類方法時,獲取類要用object_getClass((id)self)方法獲取class, 注意和通過[self class]獲取的class是不同的對象,可以通過打印hash值來看
Class class = object_getClass((id)self); // 這個地方要注意
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就像一把瑞士小刀,如果使用得當,它會有效地解決問題。但使用不當,將帶來很多麻煩。在stackoverflow上有人已經提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?,它的危險性主要體現以下幾個方面:

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 項目實踐應用

5.1 自動處理按鈕高亮和不可點擊狀態的屬性

5.2 自動采集控制器的進入,退出和銷毀等動作

5.3 自動采集按鈕的點擊事件

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,227評論 0 7
  • Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了運行時來處理。這種動態語言的...
    有一種再見叫青春閱讀 602評論 0 3
  • Objective-C語言是一門動態語言,他將很多靜態語言在編譯和鏈接時期做的事情放到了運行時來處理。這種動態語言...
    tigger丨閱讀 1,417評論 0 8
  • 原文出處:南峰子的技術博客 Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了...
    _燴面_閱讀 1,245評論 1 5