一. 基本函數 1. 根據 sel 得到 class 的實例方法 2. 根據 sel 得到 class 的函數指針 3. 給 class 添加方法 4. 替換 class 的 sel 對應的函數指針,返回值為 sel 對應的原函數指針 5. 交換兩個 method 6. 直接替換 method 的函 ...
一. 基本函數
根據 sel 得到 class 的實例方法
Method class_getInstanceMethod(Class cls, SEL name)
根據 sel 得到 class 的函數指針
IMP class_getMethodImplementation(Class cls, SEL name)
給 class 添加方法
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
替換 class 的 sel 對應的函數指針,返回值為 sel 對應的原函數指針
class_replaceMethod(Class cls, SEL name, IMP imp, const char * types)
交換兩個 method
method_exchangeImplementations(Method m1, Method m2)
直接替換 method 的函數指針
method_setImplementation(Method method, IMP imp)
二. 主要問題
1. 原子性操作問題
解決方案一般是在 `+(void)load`方法中處理;也可以加鎖;還可以在`+(void)initialize`中去做,但是一定要註意繼承的問題。
2. 改變範圍超出預期
比如你可能會只想修改一個實例的方法,但實際上你修改了所有的實例方法。比如你交換的方法真實的實現是在父類中的,你的修改會影響所有的父類派生出來的類。
例如,直接使用 `method_exchangeImplementations` 方法,考慮下這種情況
@ B
- (void)case1
{
NSLog(@"case 1 B");
}
@end
@interface C: B
@property (nonatomic, copy) NSString *x;
@end
@implementation C
- (void)case2
{
NSLog(@"case2 C %@-%@",[self class],self.x);
}
@end
- (void)someMethod {
Method a1 = class_getInstanceMethod([C class], @selector(case1));
Method a2 = class_getInstanceMethod([C class], @selector(case2));
method_exchangeImplementations(a1, a2);
B *b = [[B alloc] init];
[b case1];
}
會發生什麼呢?會 crash ,因為 C 作為 B 的子類並沒有實現 case1
方法,方法交換會把 B 的case1
替換成 C 的 case2
,後面 [b case1]
其實會執行 void _.._case2(C * self, SEL _cmd)
這個函數,裡面調用 x 屬性,所以 crash。
為了避免這個錯誤,一般的做法有,先用 class_addMethod
判斷能否添加將要替換的方法,如果可以添加,說明子類原先沒有實現此方法,這個方法是在父類中實現的。具體可以看參考1。
RSSwizzle
和 jrswizzle
都避免了這個問題。
3. 可能有命名衝突
比如你交換的方法很可能在別的地方(比如類別里)已經有同樣命名的存在了。此時的避免方法可以是直接去替換 Method 里的函數指針,保存原有的函數指針來調用:
typedef IMP *IMPPointer;
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 NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
4. 可能會使用不一樣的方法參數
比如同樣調用原來的函數時,`_cmd`已經不一樣了,解決方案可以和上面一致。
5. 類簇類的swizzling
對於 Objective-C 中的一些類簇類,比如 NSNumber、NSArray和NSMutableArray 等,因為這些並不是一個具體的類,而是一個抽象類,如果直接在這些類的內部寫個方法通過self class等方式來獲取 Class 並做方法交換的話,因為並不能獲得其真實的類名,所以會達不到想要的效果。
比如,我們可以通過以下代碼來得到NSMutableArray的真實類型:
object_getClass([[NSMutableArray alloc] init]);
objc_getClass("__NSArrayM");
上面代碼中__NSArrayM
是NSMutableArray
的真實類名;
6. 子類方法調用了 super 方法,並且都做了交換
比如下麵的例子就會發生迴圈調用。
@ A
- (void)log {
NSLog(@"i am a");
}
- (void)print {
[self print];
}
@end
@ B
- (void)log {
NSLog(@"i am b");
[super log];
}
- (void)print {
[self print];
}
@end
下麵做一下方法交換,並執行子類的方法。
- (void)test {
Method a1 = class_getInstanceMethod([A class], @selector(log));
Method a2 = class_getInstanceMethod([A class], @selector(print));
method_exchangeImplementations(a1, a2);
Method a3 = class_getInstanceMethod([B class], @selector(log));
Method a4 = class_getInstanceMethod([B class], @selector(print));
method_exchangeImplementations(a3, a4);
B *b = [[B alloc] init];
[b print];
}
方法的調用流程(用imp來表示)
B.log - A.print - B.log....
從而形成了迴圈的引用。
三. 方法交換的實現
1. 直接修改 Method 的函數指針
參考2中提到的,利用 (一、1)中的方法,額外提供一個變數來存儲原始的函數指針,如果需要調用原始方法,就用這個變數來主動設置 sel 參數來防止原始函數用到了_cmd
的情況
2. jrswizzle
主要用到了 method_exchangeImplementations
方法,魯棒性上做的工作就是先做了 class_addMethod
操作。簡單是很簡單,然而上面所說的大部分問題他都不能避免。
3. RSSwizzle
主要用到了 class_replaceMethod
方法,避免了子類的替換影響了父類。而且對方法交換加了鎖,增強了線程安全。有更多的替換選項。並且,他通過block
引入了兩個方法互相調用或者子類父類同時交換導致的迴圈問題。上面的問題幾乎都可以避免。
問題是:OSSpinLock
不被建議使用了。
官方文檔說他解決了method_exchangeImplementations
的限制:
- 只有在 +load 方法中才線程安全
- 對沒有重載的方法交換會遇到非期望的結果
- 交換的方法不能依賴
_cmd
參數 (通過RSSwizzleInfo
結構,保存原始的 selector) - 命名衝突
參考:
1.http://nshipster.cn/method-swizzling/
2.https://blog.newrelic.com/2014/04/16/right-way-to-swizzle/
3.http://yulingtianxia.com/blog/2017/04/17/Objective-C-Method-Swizzling/
4.https://stackoverflow.com/questions/5339276/what-are-the-dangers-of-method-swizzling-in-objective-c