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。

為了避免這個錯誤,一般的做法有,先用 class_addMethod 判斷能否添加將要替換的方法,如果可以添加,說明子類原先沒有實現此方法,這個方法是在父類中實現的。具體可以看參考1。

RSSwizzlejrswizzle 都避免了這個問題。

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的真實類名;

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 的限制:

  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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM