iOS 開發(fā):Runtime(詳解三) Method Swizzling

  1. Method Swizzling(動態(tài)方法交換)簡介
    Method Swizzling 用于改變一個(gè)已經(jīng)存在的 selector 實(shí)現(xiàn)艳馒。我們可以在程序運(yùn)行時(shí)缝左,通過改變 selector 所在 Class(類)的 method list(方法列表)的映射從而改變方法的調(diào)用杆融。其實(shí)質(zhì)就是交換兩個(gè)方法的 IMP(方法實(shí)現(xiàn))褥紫。
    Method(方法)對應(yīng)的是 objc_method 結(jié)構(gòu)體亿扁;而 objc_method 結(jié)構(gòu)體 中包含了 SEL method_name(方法名)如蚜、IMP method_imp(方法實(shí)現(xiàn))择镇。
// objc_method 結(jié)構(gòu)體
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法類型
    IMP _Nonnull method_imp;                     // 方法實(shí)現(xiàn)
};

Method swizzling 修改了method list(方法列表)挡逼,使得不同 Method(方法)中的鍵值對發(fā)生了交換。比如交換前兩個(gè)鍵值對分別為 SEL A :IMP A腻豌、SEL B :IMP B家坎,交換之后就變?yōu)榱? SEL A :IMP BSEL B :IMP A吝梅。如圖所示:

Method swizzling.png

  1. Method Swizzling 使用方法
    在當(dāng)前類的 + (void)load;
#import "ViewController.h"
#import <objc/runtime.h>

@interface ViewController ()

@end

@implementation ViewController
//舉例虱疏。所以在viewDidLoad中寫
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self SwizzlingMethod];
    [self originalFunction];
    [self swizzledFunction];
}
// 交換 原方法 和 替換方法 的方法實(shí)現(xiàn)
- (void)SwizzlingMethod {
    // 當(dāng)前類
    Class class = [self class];
    // 原方法名 和 替換方法名
    SEL originalSelector = @selector(originalFunction);
    SEL swizzledSelector = @selector(swizzledFunction);
    
    // 原方法結(jié)構(gòu)體 和 替換方法結(jié)構(gòu)體
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    // 調(diào)用交換兩個(gè)方法的實(shí)現(xiàn)
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替換方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end

剛才我們簡單演示了如何在當(dāng)前類中如何進(jìn)行 Method Swizzling 操作。但一般日常開發(fā)中苏携,并不是直接在原有類中進(jìn)行 Method Swizzling 操作做瞪。更多的是為當(dāng)前類添加一個(gè)分類,然后在分類中進(jìn)行 Method Swizzling 操作。另外真正使用會比上邊寫的考慮東西要多一點(diǎn)装蓬,要復(fù)雜一些著拭。

2.3 Method Swizzling 方案 A

在該類的分類中添加 Method Swizzling 交換方法,用普通方式

@implementation UIViewController (Swizzling)

// 交換 原方法 和 替換方法 的方法實(shí)現(xiàn)
+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 當(dāng)前類
        Class class = [self class];
        
        // 原方法名 和 替換方法名
        SEL originalSelector = @selector(originalFunction);
        SEL swizzledSelector = @selector(swizzledFunction);
        
        // 原方法結(jié)構(gòu)體 和 替換方法結(jié)構(gòu)體
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        /* 如果當(dāng)前類沒有 原方法的 IMP牍帚,說明在從父類繼承過來的方法實(shí)現(xiàn)儡遮,
         * 需要在當(dāng)前類中添加一個(gè) originalSelector 方法,
         * 但是用 替換方法 swizzledMethod 去實(shí)現(xiàn)它 
         */
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // 原方法的 IMP 添加成功后暗赶,修改 替換方法的 IMP 為 原始方法的 IMP
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            // 添加失斅臀(說明已包含原方法的 IMP),調(diào)用交換兩個(gè)方法的實(shí)現(xiàn)
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}
// 替換方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}
@end

2.2 Method Swizzling 方案 B

在該類的分類中添加 Method Swizzling 交換方法忆首,但是使用函數(shù)指針的方式爱榔。
方案 B 和方案 A 的最大不同之處在于使用了函數(shù)指針的方式,使用函數(shù)指針最大的好處是可以有效避免命名錯(cuò)誤糙及。

#import "UIViewController+PointerSwizzling.h"
#import <objc/runtime.h>

typedef IMP *IMPPointer;

// 交換方法函數(shù)
static void MethodSwizzle(id self, SEL _cmd, id arg1);
// 原始方法函數(shù)指針
static void (*MethodOriginal)(id self, SEL _cmd, id arg1);

// 交換方法函數(shù)
static void MethodSwizzle(id self, SEL _cmd, id arg1) {
    
    // 在這里添加 交換方法的相關(guān)代碼
    NSLog(@"swizzledFunc");
    
    MethodOriginal(self, _cmd, arg1);
}

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation UIViewController (PointerSwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzle:@selector(originalFunc) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];
    });
}

+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}

// 原始方法
- (void)originalFunc {
    NSLog(@"originalFunc");
}

@end

2.4 Method Swizzling 方案 C

在其他類中添加 Method Swizzling 交換方法

static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {
    return class_addMethod(theClass, selector,  method_getImplementation(method),  method_getTypeEncoding(method));
}

@interface _AFURLSessionTaskSwizzling : NSObject

@end

@implementation _AFURLSessionTaskSwizzling

+ (void)load {
    if (NSClassFromString(@"NSURLSessionTask")) {
        
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        Class currentClass = [localDataTask class];
        
        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }
        
        [localDataTask cancel];
        [session finishTasksAndInvalidate];
    }
}

+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
    Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
    Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));

    if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
        af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
    }

    if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
        af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
    }
}

- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_resume];
    
    if (state != NSURLSessionTaskStateRunning) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}

- (void)af_suspend {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_suspend];
    
    if (state != NSURLSessionTaskStateSuspended) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self];
    }
}

2.5 Method Swizzling 方案 D

優(yōu)秀的第三方框架:JRSwizzleRSSwizzle

  1. Method Swizzling 使用注意

3.1详幽、應(yīng)該只在 +load 中執(zhí)行 Method Swizzling。

程序在啟動的時(shí)候浸锨,會先加載所有的類唇聘,這時(shí)會調(diào)用每個(gè)類的 +load方法。而且在整個(gè)程序運(yùn)行周期只會調(diào)用一次(不包括外部顯示調(diào)用)柱搜。所以在 +load 方法進(jìn)行 Method Swizzling 再好不過了迟郎。
為什么不用 +initialize 方法呢。
因?yàn)?code>+initialize方法的調(diào)用時(shí)機(jī)是在 第一次向該類發(fā)送第一個(gè)消息的時(shí)候才會被調(diào)用聪蘸。如果該類只是引用宪肖,沒有調(diào)用,則不會執(zhí)行 +initialize 方法健爬。
Method Swizzling 影響的是全局狀態(tài)控乾,+load方法能保證在加載類的時(shí)候就進(jìn)行交換,保證交換結(jié)果娜遵。而使用 +initialize方法則不能保證這一點(diǎn)蜕衡,有可能在使用的時(shí)候起不到交換方法的作用。

3.2设拟、Method Swizzling 在 +load 中執(zhí)行時(shí)慨仿,不要調(diào)用 [super load];

上邊我們說了,程序在啟動的時(shí)候纳胧,會先加載所有的類镰吆。如果在 + (void)load方法中調(diào)用 [super load] 方法,就會導(dǎo)致父類的 Method Swizzling 被重復(fù)執(zhí)行兩次躲雅,而方法交換也被執(zhí)行了兩次鼎姊,相當(dāng)于互換了一次方法之后,第二次又換回去了,從而使得父類的 Method Swizzling 失效相寇。

3.3慰于、Method Swizzling 應(yīng)該總是在 dispatch_once 中執(zhí)行。

Method Swizzling 不是原子操作唤衫,dispatch_once 可以保證即使在不同的線程中也能確保代碼只執(zhí)行一次婆赠。所以,我們應(yīng)該總是在 dispatch_once 中執(zhí)行 Method Swizzling 操作佳励,保證方法替換只被執(zhí)行一次休里。

3.4、使用 Method Swizzling 后要記得調(diào)用原生方法的實(shí)現(xiàn)赃承。

在交換方法實(shí)現(xiàn)后記得要調(diào)用原生方法的實(shí)現(xiàn)(除非你非常確定可以不用調(diào)用原生方法的實(shí)現(xiàn)):APIs 提供了輸入輸出的規(guī)則妙黍,而在輸入輸出中間的方法實(shí)現(xiàn)就是一個(gè)看不見的黑盒。交換了方法實(shí)現(xiàn)并且一些回調(diào)方法不會調(diào)用原生方法的實(shí)現(xiàn)這可能會造成底層實(shí)現(xiàn)的崩潰瞧剖。

3.5避免命名沖突和參數(shù) _cmd 被篡改拭嫁。

1.避免命名沖突一個(gè)比較好的做法是為替換的方法加個(gè)前綴以區(qū)別原生方法。一定要確保調(diào)用了原生方法的所有地方不會因?yàn)樽约航粨Q了方法的實(shí)現(xiàn)而出現(xiàn)意料不到的結(jié)果抓于。
在使用 Method Swizzling 交換方法后記得要在交換方法中調(diào)用原生方法的實(shí)現(xiàn)做粤。在交換了方法后并且不調(diào)用原生方法的實(shí)現(xiàn)可能會造成底層實(shí)現(xiàn)的崩潰。
2.避免方法命名沖突另一個(gè)更好的做法是使用函數(shù)指針捉撮,也就是上邊提到的 方案 B怕品,這種方案能有效避免方法命名沖突和參數(shù) _cmd 被篡改。

3.6巾遭、謹(jǐn)慎對待 Method Swizzling肉康。

使用 Method Swizzling,會改變非自己擁有的代碼恢总。我們使用 Method Swizzling 通常會更改一些系統(tǒng)框架的對象方法迎罗,或是類方法。我們改變的不只是一個(gè)對象實(shí)例片仿,而是改變了項(xiàng)目中所有的該類的對象實(shí)例,以及所有子類的對象實(shí)例尤辱。所以砂豌,在使用 Method Swizzling 的時(shí)候,應(yīng)該保持足夠的謹(jǐn)慎光督。

3.7阳距、對于 Method Swizzling 來說,調(diào)用順序 很重要结借。

load 方法的調(diào)用規(guī)則為:
1筐摘、先調(diào)用主類,按照編譯順序,順序地根據(jù)繼承關(guān)系由父類向子類調(diào)用咖熟;
2圃酵、再調(diào)用分類,按照編譯順序馍管,依次調(diào)用郭赐;
3、+ load 方法除非主動調(diào)用确沸,否則只會調(diào)用一次捌锭。
這樣的調(diào)用規(guī)則導(dǎo)致了 + load 方法調(diào)用順序并不一定確定。一個(gè)順序可能是:父類 -> 子類 -> 父類類別 -> 子類類別罗捎,也可能是 父類 -> 子類 -> 子類類別 -> 父類類別观谦。所以 Method Swizzling 的順序不能保證,那么就不能保證 Method Swizzling 后方法的調(diào)用順序是正確的桨菜。
所以被用于 Method Swizzling 的方法必須是當(dāng)前類自身的方法坎匿,如果把繼承父類來的 IMP 復(fù)制到自身上面可能會存在問題。如果 + load 方法調(diào)用順序?yàn)椋焊割?-> 子類 -> 父類類別 -> 子類類別雷激,那么造成的影響就是調(diào)用子類的替換方法并不能正確調(diào)起父類分類的替換方法替蔬。

  1. Method Swizzling 應(yīng)用場景

Method Swizzling在開發(fā)中更多的是應(yīng)用于系統(tǒng)類庫,以及第三方框架的方法替換屎暇。在官方不公開源碼的情況下承桥,我們可以借助 Runtime 的 Method Swizzling 為原有方法添加額外的功能。

1根悼、 全局頁面統(tǒng)計(jì)功能
2凶异、字體根據(jù)屏幕尺寸適配
3、處理按鈕重復(fù)點(diǎn)擊
4挤巡、TableView剩彬、CollectionView 異常加載占位圖
5、APM(應(yīng)用性能管理)矿卑、防止程序崩潰

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喉恋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子母廷,更是在濱河造成了極大的恐慌轻黑,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琴昆,死亡現(xiàn)場離奇詭異氓鄙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)业舍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進(jìn)店門抖拦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來升酣,“玉大人,你說我怎么就攤上這事态罪∝眩” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵向臀,是天一觀的道長巢墅。 經(jīng)常有香客問我,道長券膀,這世上最難降的妖魔是什么君纫? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮芹彬,結(jié)果婚禮上蓄髓,老公的妹妹穿的比我還像新娘。我一直安慰自己舒帮,他們只是感情好会喝,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著玩郊,像睡著了一般肢执。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上译红,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天预茄,我揣著相機(jī)與錄音,去河邊找鬼侦厚。 笑死耻陕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刨沦。 我是一名探鬼主播诗宣,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼想诅!你這毒婦竟也來了召庞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤侧蘸,失蹤者是張志新(化名)和其女友劉穎裁眯,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讳癌,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年存皂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侧戴。...
    茶點(diǎn)故事閱讀 40,861評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鳞青,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出它改,到底是詐尸還是另有隱情,我是刑警寧澤商乎,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布央拖,位于F島的核電站,受9級特大地震影響鹉戚,放射性物質(zhì)發(fā)生泄漏鲜戒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一抹凳、第九天 我趴在偏房一處隱蔽的房頂上張望遏餐。 院中可真熱鬧,春花似錦赢底、人聲如沸失都。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粹庞。三九已至,卻和暖如春洽损,著一層夾襖步出監(jiān)牢的瞬間庞溜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工趁啸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留强缘,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓不傅,卻偏偏與公主長得像旅掂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子访娶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評論 2 361

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