方法交換
- method-swizzling是什么
- 相關API
- 方法交換的風險
- method-swizzling使用過程中的一次性問題
- 在當前類中進行方法交換
- 類方法的方法交換
- 方法交換的應用
method-swizzling是什么
method-swizzling
的含義是方法交換,他的主要作用是在運行的時候將一個方法的實現替換為另一個方法的實現,這就是我們說的iOS黑魔法。
OC中,利用
method-swizzling
實現AOP,AOP(面向切面編程)是一種編程思想,區別于OOP。其中,
AOP
是面向切面進行提取封裝,提取各個模塊中的公共部分,提高模塊的復用率,降低業務之間的耦合性;而OOP
更加傾向于對業務模塊的封裝
,劃分出更加清晰的邏輯單元。
在之前學習探索消息流程的時候,我們了解到可以通過SEL方法查找器來查找method
方法,而后得到對應的IMP
。而方法交換其實就是將SEL與IMP原本的對應斷開,并將SEL和新的IMP生成對應關系。
這里筆者附上一張看到的圖片來解釋其關系:
相關API
//通過sel獲取方法Method
class_getInstanceMethod://獲取實例方法
class_getClassMethod://獲取類方法method_getImplementation://獲取一個方法的實現
method_setImplementation://設置一個方法的實現
method_getTypeEncoding://獲取方法實現的編碼類型
class_addMethod://添加方法實現
class_replaceMethod://用一個方法的實現,替換另一個方法的實現,即aIMP 指向 bIMP,但是bIMP不一定指向aIMP
method_exchangeImplementations://交換兩個方法的實現,即 aIMP -> bIMP, bIMP -> aIMP
方法交換的風險
下面我們來看看方法交換中會遇到的問題
method-swizzling使用過程中的一次性問題
一次性:method-swizzling
寫在load
方法中,但是load
會主動的調用多次,這會導致方法的重復交換,令方法SEL指向又恢復成原來的IMP的問題出現。
故而,這里我們可以通過單例模式的原則,令方法交換僅僅執行一次,這里我們需要使用GCD來實現單例,下面舉一個例子說明該問題:
+ (void)load{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{[LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];});
}
在當前類中進行方法交換
當我們進行方法交換的時候,必須交換的是當前類中的方法,若是方法在父類中,直接交換會導致父類的方法被錯誤的修改,這樣會影響其所有的子類。
- 方法交換的隔離性:必須確保交換的目標方法是當前類已實現或動態添加的方法,避免污染父類方法列表。
- 動態注冊的重要性:通過
class_addMethod
隔離父類實現,保證交換僅作用于當前類及其子類。- 風險規避:在子類分類中操作父類方法需謹慎,推薦使用
method_setImplementation
或class_replaceMethod
控制影響范圍。
這里我們先來看看若是直接和其父類進行方法交換會引起的后果:
上面的是父類中的方法,下面在子類的分類中方法交換,我們來看看會發生什么
根據斷點顯示,我們可以發現在子類Student
中方法交換之后子類調用最后結果是正確的,但是到了最后一個斷點的時候,可以發現程序報錯了。我們來看看這是為什么?
這里我們先看子類
Student
調用personInstanceMethod
方法,由于其imp交換成了lg_studentInstanceMethod
,而在子類的分類中有該方法,所以不會報錯。但是當父類Person
中的imp也被交換成了lg_studentInstanceMethod
,但我們并沒有在父類中實現該方法,即相關的imp無法找到,就會導致程序崩潰掉。
那么我們怎么做就可以讓程序不崩潰呢,這里我們可以通過class_addMethod
嘗試添加要交換的方法,下面先給出示例:
一般交換方法: 交換自己有的方法 – 走下面 因為自己有意味添加方法失敗
交換自己沒有實現的方法:
-
首先第一步:會先嘗試給自己添加要交換的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
-
然后再將父類的IMP給swizzle personInstanceMethod(imp) -> swizzledSEL
當我們兩個方法都沒有實現的情況下,就會進入無限遞歸,導致最后棧溢出:
原因是
棧溢出
,遞歸死循環
了,那么為什么會發生遞歸呢?----主要是因為personInstanceMethod
沒有實現,然后在方法交換時,始終都找不到oriMethod,然后交換了寂寞,即交換失敗,當我們調用personInstanceMethod(oriMethod)
時,也就是oriMethod
會進入LG中lg_studentInstanceMethod
方法,然后這個方法中又調用了lg_studentInstanceMethod
,此時的lg_studentInstanceMethod
并沒有指向oriMethod
,然后導致了自己調自己
,即遞歸死循環
類方法的方法交換
其實類方法的方法交換和實例方法的方法交換差不多,這里我給出一個例子,這里和實例方法的區別其實只是類方法存在元類中。
+ (void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{if (!cls) NSLog(@"傳入的交換類不能為空");Method oriMethod = class_getClassMethod([cls class], oriSEL);Method swiMethod = class_getClassMethod([cls class], swizzledSEL);if (!oriMethod) { // 避免動作沒有意義// 在oriMethod為nil時,替換后將swizzledSEL復制一個不做任何事的空實現,代碼如下:class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){NSLog(@"來了一個空的 imp");}));}// 一般交換方法: 交換自己有的方法 -- 走下面 因為自己有意味添加方法失敗// 交換自己沒有實現的方法:// 首先第一步:會先嘗試給自己添加要交換的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)// 然后再將父類的IMP給swizzle personInstanceMethod(imp) -> swizzledSEL//oriSEL:personInstanceMethodBOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));if (didAddMethod) {class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));}else{method_exchangeImplementations(oriMethod, swiMethod);}}
方法交換的應用
方法交換最常用的一個應用是防止數組、字典等越界崩潰。在iOS中,NSNumber
、NSArray
、NSDictionary
這些都是類簇,一個NSArray的實現可能由多個類組成,所以我們必須獲取到其"真身"進行交換,直接對NSarray
進行操作是無效的。
下面列舉了NSArray和NSDictionary本類的類名,可以通過Runtime函數取出本類。
類名 | 真身 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
這里我以NSArray
為例來看看方法交換的應用:
#import "NSArray+crush.h"
#import "objc/objc-runtime.h"
@implementation NSArray (crush)
+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{NSLog(@"load");Method fromMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndex:));Method toMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(new_objectAtIndex:));method_exchangeImplementations(fromMethod, toMethod);});}
- (id)new_objectAtIndex:(NSUInteger)index {NSLog(@"new_objectAtIndex");if (index >= self.count) {// 越界處理NSLog(@"Index %lu out of bounds, array count is %lu.", (unsigned long)index, (unsigned long)self.count);return nil;} else {// 正常訪問,注意這里調用的是替換后的方法,因為實現已經交換return [self new_objectAtIndex:index];}}@end
這里我們新建一個NSArray
的分類,交換一下objectAtIndex
和new_objectAtIndex
方法,我們來看看結果:
這里的NSArray類型是NSConstantArray
,雖然我們已經越界了但是程序并沒有退出,我們打印了一個報錯,這樣就保證了這個函數的安全.