Objective-C 方法交換實踐(二) - 方法指針交換

一. 基本函數

  1. 根據 sel 獲得 class 的實例方法
Method class_getInstanceMethod(Class cls, SEL name)
  1. 根據 sel 獲得 class 的函數指針
IMP class_getMethodImplementation(Class cls, SEL name)
  1. 給 class 添加方法
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
  1. 替換 class 的 sel 對應的函數指針,返回值爲 sel 對應的原函數指針
class_replaceMethod(Class cls, SEL name, IMP imp, const char * types)
  1. 交換兩個 method
method_exchangeImplementations(Method m1, Method m2)
  1. 直接替換 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。git

爲了不這個錯誤,通常的作法有,先用 class_addMethod 判斷可否添加將要替換的方法,若是能夠添加,說明子類原先沒有實現此方法,這個方法是在父類中實現的。具體能夠看參考1。github

RSSwizzlejrswizzle 都避免了這個問題。objective-c

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");

上面代碼中__NSArrayMNSMutableArray的真實類名;atom

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

從而造成了循環的引用。code

三. 方法交換的實現

1. 直接修改 Method 的函數指針

參考2中提到的,利用 (1、1)中的方法,額外提供一個變量來存儲原始的函數指針,若是須要調用原始方法,就用這個變量來主動設置 sel 參數來防止原始函數用到了_cmd 的狀況blog

2. jrswizzle

主要用到了 method_exchangeImplementations 方法,魯棒性上作的工做就是先作了 class_addMethod 操做。簡單是很簡單,然而上面所說的大部分問題他都不能避免。

3. RSSwizzle

主要用到了 class_replaceMethod 方法,避免了子類的替換影響了父類。並且對方法交換加了鎖,加強了線程安全。有更多的替換選項。而且,他經過block引入了兩個方法互相調用或者子類父類同時交換致使的循環問題。上面的問題幾乎均可以免。
問題是:OSSpinLock 不被建議使用了。
官方文檔說他解決了method_exchangeImplementations 的限制:

  1. 只有在 +load 方法中才線程安全
  2. 對沒有重載的方法交換會遇到非指望的結果
  3. 交換的方法不能依賴 _cmd 參數 (經過 RSSwizzleInfo結構,保存原始的 selector)
  4. 命名衝突

參考:
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

相關文章
相關標籤/搜索