Runtime Method Swizzling開發實例彙總

目前已更新實例彙總:
  • 實例一:替換ViewController生命週期方法
  • 實例二:解決獲取索引、添加、刪除元素越界崩潰問題
  • 實例三:防止按鈕重複暴力點擊
  • 實例四:全局更換控件初始效果
  • 實例五:App熱修復
  • 實例六:App異常加載佔位圖通用類封裝
  • 實例七:全局修改導航欄後退(返回)按鈕
Method Swizzling通用方法封裝

在列舉之前,我們可以將Method Swizzling功能封裝爲類方法,作爲NSObject的類別,這樣我們後續調用也會方便些。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface NSObject (Swizzling) 

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                         bySwizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
@implementation NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{
    Class class = [self class];
    //原有方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    //替換原有方法的新方法
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    //先嚐試給源SEL添加IMP,這裏是爲了避免源SEL沒有實現IMP的情況
    BOOL didAddMethod = class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {//添加成功:說明源SEL沒有實現IMP,將源SEL的IMP替換到交換SEL的IMP
        class_replaceMethod(class,swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {//添加失敗:說明源SEL已經有IMP,直接將兩個SEL的IMP交換即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end
⚠️補充知識點
  • SEL、Method、IMP的含義及區別

    在運行時,類(Class)維護了一個消息分發列表來解決消息的正確發送。每一個消息列表的入口是一個方法(Method),這個方法映射了一對鍵值對,其中鍵是這個方法的名字(SEL),值是指向這個方法實現的函數指針 implementation(IMP)。
    僞代碼表示:

    Class {
          MethodList (
                      Method{
                          SEL:IMP;
                      }
                      Method{
                          SEL:IMP;
                      }
                      );
          };

    Method Swizzling就是改變類的消息分發列表來讓消息解析時從一個選擇器(SEL)對應到另外一個的實現(IMP),同時將原始的方法實現混淆到一個新的選擇器(SEL)。

  • 爲什麼要添加didAddMethod判斷?

    先嚐試添加原SEL其實是爲了做一層保護,因爲如果這個類沒有實現originalSelector,但其父類實現了,那class_getInstanceMethod會返回父類的方法。這樣method_exchangeImplementations替換的是父類的那個方法,這當然不是我們想要的。所以我們先嚐試添加 orginalSelector,如果已經存在,再用 method_exchangeImplementations 把原方法的實現跟新的方法實現給交換掉。
    如果理解還不夠透徹,我們可以進入runtime.h中查看class_addMethod源碼解釋:

    /** 
    * 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.
    */

    大概的意思就是我們可以通過class_addMethod爲一個類添加方法(包括方法名稱(SEL)和方法的實現(IMP)),返回值爲BOOL類型,表示方法是否成功添加。需要注意的地方是class_addMethod會添加一個覆蓋父類的實現,但不會取代原有類的實現。也就是說如果class_addMethod返回YES,說明子類中沒有方法originalSelector,通過class_addMethod爲其添加了方法originalSelector,並使其實現(IMP)爲我們想要替換的實現。

    class_addMethod(class,originalSelector,
                                          method_getImplementation(swizzledMethod),
                                          method_getTypeEncoding(swizzledMethod));

    同時再將原有的實現(IMP)替換到swizzledMethod方法上,

    class_replaceMethod(class,swizzledSelector,
                              method_getImplementation(originalMethod),
                              method_getTypeEncoding(originalMethod));

    從而實現了方法的交換,並且未影響父類方法的實現。反之如果class_addMethod返回NO,說明子類中本身就具有方法originalSelector的實現,直接調用交換即可。

    method_exchangeImplementations(originalMethod, swizzledMethod);

    這一部分內容比較繞口,希望大家可以耐下心來仔細反覆閱讀。

-------------------------------實例列舉-------------------------------
實例一:替換ViewController生命週期方法

App跳轉到某具有網絡請求的界面時,爲了用戶體驗效果常會添加加載欄或進度條來顯示當前請求情況或進度。這種界面都會存在這樣一個問題,在請求較慢時,用戶手動退出界面,這時候需要去除加載欄。
當然可以依次在每個界面的viewWillDisappear方法中添加去除方法,但如果類似的界面過多,一味的複製粘貼也不是方法。這時候就能體現Method Swizzling的作用了,我們可以替換系統的viewWillDisappear方法,使得每當執行該方法時即自動去除加載欄。

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)];
    });
}

- (void)sure_viewWillDisappear:(BOOL)animated {
    [self sure_viewWillDisappear:animated];
    [SVProgressHUD dismiss];
}

代碼如上,這樣就不用考慮界面是否移除加載欄的問題了。補充一點,通常我們也會在生命週期方法中設置默認界面背景顏色,因若背景顏色默認爲透明對App的性能也有一定影響,這大家可以在UIKit性能優化那篇文章中查閱。但類似該類操作也可以書寫在通用類中,所以具體使用還要靠自己定奪。

⚠️補充知識點
  • 爲什麼方法交換調用在+load方法中?
    在Objective-C runtime會自動調用兩個類方法,分別爲+load與+ initialize。+load 方法是在類被加載的時候調用的,也就是一定會被調用。而+initialize方法是在類或它的子類收到第一條消息之前被調用的,這裏所指的消息包括實例方法和類方法的調用。也就是說+initialize方法是以懶加載的方式被調用的,如果程序一直沒有給某個類或它的子類發送消息,那麼這個類的+initialize方法是永遠不會被調用的。此外+load方法還有一個非常重要的特性,那就是子類、父類和分類中的+load方法的實現是被區別對待的。換句話說在 Objective-C runtime自動調用+load方法時,分類中的+load方法並不會對主類中的+load方法造成覆蓋。綜上所述,+load 方法是實現 Method Swizzling 邏輯的最佳“場所”。如需更深入理解,可參考Objective-C 深入理解 +load 和 +initialize
  • 爲什麼方法交換要在dispatch_once中執行?
    方法交換應該要線程安全,而且保證在任何情況下(多線程環境,或者被其他人手動再次調用+load方法)只交換一次,防止再次調用又將方法交換回來。除非只是臨時交換使用,在使用完成後又交換回來。 最常用的解決方案是在+load方法中使用dispatch_once來保證交換是安全的。之前有讀者反饋+load方法本身即爲線程安全,爲什麼仍需添加dispatch_once,其原因就在於+load方法本身無法保證其中代碼只被執行一次。
  • 爲什麼沒有發生死循環?
    一定有很多讀者有疑惑,爲什麼sure_viewWillDisappear方法中的代碼沒有發生遞歸死循環。其原因很簡單,因爲方法已經執行過交換,調用[self sure_viewWillDisappear:animated]本質是在調用原有方法viewWillDisappear,反而如果我們在方法中調用[self viewWillDisappear:animated]才真的會發生死循環。是不是很繞?仔細看看。
實例二:解決獲取索引、添加、刪除元素越界崩潰問題

對於NSArray、NSDictionary、NSMutableArray、NSMutableDictionary不免會進行索引訪問、添加、刪除元素的操作,越界問題也是很常見,這時我們可以通過Method Swizzling解決這些問題,越界給予提示防止崩潰。

這裏以NSMutableArray爲例說明

#import "NSMutableArray+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation NSMutableArray (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObject:) bySwizzledSelector:@selector(safeRemoveObject:) ];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(addObject:) bySwizzledSelector:@selector(safeAddObject:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObjectAtIndex:) bySwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(insertObject:atIndex:) bySwizzledSelector:@selector(safeInsertObject:atIndex:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(objectAtIndex:) bySwizzledSelector:@selector(safeObjectAtIndex:)];
    });
}
- (void)safeAddObject:(id)obj {
    if (obj == nil) {
        NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
    } else {
        [self safeAddObject:obj];
    }
}
- (void)safeRemoveObject:(id)obj {
    if (obj == nil) {
        NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
        return;
    }
    [self safeRemoveObject:obj];
}
- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
    if (anObject == nil) {
        NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
    } else if (index > self.count) {
        NSLog(@"%s index is invalid", __FUNCTION__);
    } else {
        [self safeInsertObject:anObject atIndex:index];
    }
}
- (id)safeObjectAtIndex:(NSUInteger)index {
    if (self.count == 0) {
        NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
        return nil;
    }
    if (index > self.count) {
        NSLog(@"%s index out of bounds in array", __FUNCTION__);
        return nil;
    }
    return [self safeObjectAtIndex:index];
}
- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
    if (self.count <= 0) {
        NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
        return;
    }
    if (index >= self.count) {
        NSLog(@"%s index out of bound", __FUNCTION__);
        return;
    }
    [self safeRemoveObjectAtIndex:index];
}
@end

對應大家可以舉一反三,相應的實現添加、刪除等,以及NSArray、NSDictionary等操作,因代碼篇幅較大,這裏就不一一書寫了。
這裏沒有使用self來調用,而是使用objc_getClass("__NSArrayM")來調用的。因爲NSMutableArray的真實類只能通過後者來獲取,而不能通過[self class]來獲取,而method swizzling只對真實的類起作用。這裏就涉及到一個小知識點:類簇。補充以上對象對應類簇表。


類簇表.png
實例三:防止按鈕重複暴力點擊

程序中大量按鈕沒有做連續響應的校驗,連續點擊出現了很多不必要的問題,例如發表帖子操作,用戶手快點擊多次,就會導致同一帖子發佈多次。

#import <UIKit/UIKit.h>
//默認時間間隔
#define defaultInterval 1
@interface UIButton (Swizzling)
//點擊間隔
@property (nonatomic, assign) NSTimeInterval timeInterval;
//用於設置單個按鈕不需要被hook
@property (nonatomic, assign) BOOL isIgnore;
@end
#import "UIButton+Swizzling.h"
#import "NSObject+Swizzling.h"

@implementation UIButton (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)];
    });
}

- (NSTimeInterval)timeInterval{
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval{
    objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}
//當按鈕點擊事件sendAction 時將會執行sure_SendAction
- (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    if (self.isIgnore) {
        //不需要被hook
        [self sure_SendAction:action to:target forEvent:event];
        return;
    }
    if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) {
        self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval;
        if (self.isIgnoreEvent){
            return;
        }else if (self.timeInterval > 0){
            [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval];
        }
    }
    //此處 methodA和methodB方法IMP互換了,實際上執行 sendAction;所以不會死循環
    self.isIgnoreEvent = YES;
    [self sure_SendAction:action to:target forEvent:event];
}
//runtime 動態綁定 屬性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
    // 注意BOOL類型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯,否則set方法會賦值出錯
    objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
    //_cmd == @select(isIgnore); 和set方法裏一致
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setIsIgnore:(BOOL)isIgnore{
    // 注意BOOL類型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯,否則set方法會賦值出錯
    objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnore{
    //_cmd == @select(isIgnore); 和set方法裏一致
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)resetState{
    [self setIsIgnoreEvent:NO];
}
@end
實例四:全局更換控件初始效果

以UILabel爲例,在項目比較成熟的基礎上,應用中需要引入新的字體,需要更換所有Label的默認字體,但是同時,對於一些特殊設置了字體的label又不需要更換。乍看起來,這個問題確實十分棘手,首先項目比較大,一個一個設置所有使用到的label的font工作量是巨大的,並且在許多動態展示的界面中,可能會漏掉一些label,產生bug。其次,項目中的label來源並不唯一,有用代碼創建的,有xib和storyBoard中的,這也將浪費很大的精力。這時Method Swizzling可以解決此問題,避免繁瑣的操作。

#import "UILabel+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UILabel (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(init) bySwizzledSelector:@selector(sure_Init)];
        [self methodSwizzlingWithOriginalSelector:@selector(initWithFrame:) bySwizzledSelector:@selector(sure_InitWithFrame:)];
        [self methodSwizzlingWithOriginalSelector:@selector(awakeFromNib) bySwizzledSelector:@selector(sure_AwakeFromNib)];
    });
}
- (instancetype)sure_Init{
    id __self = [self sure_Init];
    UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}
- (instancetype)sure_InitWithFrame:(CGRect)rect{
    id __self = [self sure_InitWithFrame:rect];
    UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}
- (void)sure_AwakeFromNib{
    [self sure_AwakeFromNib];
    UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
}
@end

這一實例個人認爲使用率可能不高,對於產品的設計這些點都是已經確定好的,更改的機率很低。況且我們也可以使用appearance來進行統一設置。

實例五:App熱修復

因爲AppStore上線審覈時間較長,且如果在線上版本出現bug修復起來也是很困難,這時App熱修復就可以解決此問題。熱修復即在不更改線上版本的前提下,對線上版本進行更新甚至添加模塊。國內比較好的熱修復技術:JSPatch。JSPatch能做到通過JS調用和改寫OC方法最根本的原因是Objective-C是動態語言,OC上所有方法的調用/類的生成都通過Objective-C Runtime在運行時進行,我們可以通過類名/方法名反射得到相應的類和方法,進而替換出現bug的方法或者添加方法等。bang的博客上有詳細的描述有興趣可以參考,這裏就不贅述了。

實例六:App異常加載佔位圖通用類封裝(更新於:2016/12/01)

詳情可見文章:《零行代碼爲App添加異常加載佔位圖》
在該功能模塊中,使用Runtime Method Swizzling進行替換tableView、collectionView的reloadData方法,使得每當執行刷新操作時,自動檢測當前組數與行數,從而實現零代碼判斷佔位圖是否顯示的功能,同樣也適用於網絡異常等情況,詳細設置可前往閱讀。

實例七:全局修改導航欄後退(返回)按鈕(更新於:2016/12/05)

在真實項目開發中,會全局統一某控件樣式,以導航欄後退(返回)按鈕爲例,通常項目中會固定爲返回字樣,或者以圖片進行顯示等。

iOS默認的返回按鈕樣式如下,默認爲藍色左箭頭,文字爲上一界面標題文字。


默認返回按鈕樣式

這裏我們仍可以通過Runtime Method Swizzling來實現該需求,在使用Method Swizzling進行更改之前,必須考慮注意事項,即儘可能的不影響原有操作,比如對於系統默認的返回按鈕,與其對應的是有界面邊緣右滑返回功能的,因此我們進行統一更改後不可使其功能廢棄。

閒話少說,我們創建基於UINavigationItem的類別,在其load方法中替換方法backBarButtonItem
代碼如下

#import "UINavigationItem+Swizzling.h"
#import "NSObject+Swizzling.h"
static char *kCustomBackButtonKey;
@implementation UINavigationItem (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(backBarButtonItem)
                               bySwizzledSelector:@selector(sure_backBarButtonItem)];

    });
}

- (UIBarButtonItem*)sure_backBarButtonItem {
    UIBarButtonItem *backItem = [self sure_backBarButtonItem];
    if (backItem) {
        return backItem;
    }
    backItem = objc_getAssociatedObject(self, &kCustomBackButtonKey);
    if (!backItem) {
        backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL];
        objc_setAssociatedObject(self, &kCustomBackButtonKey, backItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return backItem;
}
@end

這裏進行將返回按鈕的文字清空操作,其他需求樣式大家也可隨意替換,現在再次運行程序,就會發現所有的返回按鈕均只剩左箭頭,並右滑手勢依然有效。如圖所示

發佈了95 篇原創文章 · 獲贊 20 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章