- 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 B
、SEL B
:IMP A
吝梅。如圖所示:
- 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
- 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)起父類分類的替換方法替蔬。
- 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)用性能管理)矿卑、防止程序崩潰