因為Objective-C的runtime機制, Method Swizzling這個黑魔法解決了我們實際開發中諸多常規手段所無法解決的問題, 比如代碼的插樁,Hook,Patch等等. 我們首先看看常規的Method Swizzling是怎樣用的, NSHipster有一篇介紹基本用法的文章Method Swizzling, 我們就先以這篇文章中的示例開始說起吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); }
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated { [self xxx_viewWillAppear:animated]; NSLog(@"viewWillAppear: %@", self); }
@end
|
簡要說明一下以上代碼的幾個重點:
- 通過在Category的
+ (void)load
方法中添加Method Swizzling的代碼,在類初始加載時自動被調用,load方法按照父類到子類,類自身到Category的順序被調用.
- 在dispatch_once中執行Method Swizzling是一種防護措施,以保證代碼塊只會被執行一次並且線程安全,不過此處並不需要,因為當前Category中的load方法並不會被多次調用.
- 嘗試先調用class_addMethod方法,以保證即便originalSelector只在父類中實現,也能達到Method Swizzling的目的.
xxx_viewWillAppear:
方法中[self xxx_viewWillAppear:animated];
代碼並不會造成死循環,因為Method Swizzling之后, 調用xxx_viewWillAppear:
實際執行的代碼已經是原來viewWillAppear中的代碼了.
其實以上的代碼也可以簡寫為以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
+ (void)load { Class class = [self class]; SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(xxx_viewWillAppear:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); if (!originalMethod || !swizzledMethod) { return; } IMP originalIMP = method_getImplementation(originalMethod); IMP swizzledIMP = method_getImplementation(swizzledMethod); const char *originalType = method_getTypeEncoding(originalMethod); const char *swizzledType = method_getTypeEncoding(swizzledMethod); class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType); class_replaceMethod(class,swizzledSelector,originalIMP,originalType); }
|
這是因為class_replaceMethod
方法其實能夠覆蓋到class_addMethod
和method_setImplementation
兩種場景, 對於第一個class_replaceMethod
來說, 如果viewWillAppear:
實現在父類, 則執行class_addMethod
, 否則就執行method_setImplementation
將原方法的IMP指定新的代碼塊; 而第二個class_replaceMethod
完成的工作便只是將新方法的IMP指向原來的代碼.
除了以上的場景之外,其它場景下我們如何使用Method Swizzling呢?
1.在不同類之間實現Method Swizzling
上面示例是通過Category來新增一個方法然后實現Method Swizzling的, 但有一些場景可能並不適合使用Category(比如私有的類,未獲取到該類的聲明), 此時我們應該如何來做Method Swizzling呢?
例如已知一個className為Car
的類中有一個實例方法- (void)run:(double)speed
, 目前需要Hook該方法對速度小於120才執行run的代碼, 按照方法交換的流程, 代碼應該是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
#import <objc/runtime.h>
@interface MyCar : NSObject @end
@implementation MyCar
+ (void)load { Class originalClass = NSClassFromString(@"Car"); Class swizzledClass = [self class]; SEL originalSelector = NSSelectorFromString(@"run:"); SEL swizzledSelector = @selector(xxx_run:); Method originalMethod = class_getInstanceMethod(originalClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector); |
與之前的流程相比,在前面添加了兩個邏輯:
- 利用runtime向目標類
Car
動態添加了一個新的方法,此時Car
類與MyCar
類一樣具備了xxx_run:
這個方法,MyCar
的利用價值便結束了;
- 為了完成后續
Car
類中run:
與xxx_run:
的方法交換,此時需要更新swizzledMethod變量為Car
中的xxx_run:
方法所對應的Method.
以上所有的邏輯也可以合並簡化為以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
+ (void)load { Class originalClass = NSClassFromString(@"Car"); Class swizzledClass = [self class]; SEL originalSelector = NSSelectorFromString(@"run:"); SEL swizzledSelector = @selector(xxx_run:); Method originalMethod = class_getInstanceMethod(originalClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector); IMP originalIMP = method_getImplementation(originalMethod); IMP swizzledIMP = method_getImplementation(swizzledMethod); const char *originalType = method_getTypeEncoding(originalMethod); const char *swizzledType = method_getTypeEncoding(swizzledMethod); class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType); class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType); }
|
簡化后的代碼便與之前使用Category的方式並沒有什么差異, 這樣代碼就很容易覆蓋到這兩種場景了, 但我們需要明確此時class_replaceMethod
所完成的工作卻是不一樣的.
- 第一個
class_replaceMethod
與之前的邏輯一致, 當run:
方法是實現在Car
類或Car
的父類, 分別執行method_setImplementation
或class_addMethod
;
- 第二個
class_replaceMethod
則直接在Car
類中注冊了xxx_run:
方法, 並且指定的IMP為當前run:
方法的IMP;
2.如何實現類方法的Method Swizzling
以上的代碼都是實現的對實例方法的交換, 那如何來實現對類方法的交換呢, 依舊直接貼代碼吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
@interface NSDictionary (Test) @end
@implementation NSDictionary (Test)
+ (void)load { Class cls = [self class]; SEL originalSelector = @selector(dictionary); SEL swizzledSelector = @selector(xxx_dictionary); // 使用class_getClassMethod來獲取類方法的Method Method originalMethod = class_getClassMethod(cls, originalSelector); Method swizzledMethod = class_getClassMethod(cls, swizzledSelector); if (!originalMethod || !swizzledMethod) { return; } IMP originalIMP = method_getImplementation(originalMethod); IMP swizzledIMP = method_getImplementation(swizzledMethod); const char *originalType = method_getTypeEncoding(originalMethod); const char *swizzledType = method_getTypeEncoding(swizzledMethod); // 類方法添加,需要將方法添加到MetaClass中 Class metaClass = objc_getMetaClass(class_getName(cls)); class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType); class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType); }
+ (id)xxx_dictionary { id result = [self xxx_dictionary]; return result; }
@end
|
相比實例方法的Method Swizzling,流程有兩點差異:
- 獲取Method的方法變更為
class_getClassMethod(Class cls, SEL name)
,從函數命名便直觀體現了和class_getInstanceMethod(Class cls, SEL name)
的差別;
- 對於類方法的動態添加,需要將方法添加到MetaClass中,因為實例方法記錄在class的method-list中, 類方法是記錄在meta-class中的method-list中的.
3.在類簇中如何實現Method Swizzling
在上面的代碼中我們實現了對NSDictionary
中的+ (id)dictionary
方法的交換,但如果我們用類似代碼嘗試對- (id)objectForKey:(id)key
方法進行交換后, 你便會發現這似乎並沒有什么用.
這是為什么呢? 平常我們在Xcode調試時,在下方Debug區域左側的Variables View中,常常會發現如__NSArrayI
或是__NSCFConstantString
這樣的Class類型, 這便是在Foundation框架中被廣泛使用的類簇, 詳情請參看Apple文檔class cluster的內容.
所以針對類簇的Method Swizzling問題就轉變為如何對這些類簇中的私有類做Method Swizzling, 在上面介紹的不同類之間做Method Swizzling便已經能解決該問題, 下面一個簡單的示例通過交換NSMutableDictionary
的setObject:forKey:
方法,讓調用這個方法時當參數object或key為空的不會拋出異常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
@interface MySafeDictionary : NSObject @end
@implementation MySafeDictionary
+ (void)load { Class originalClass = NSClassFromString(@"__NSDictionaryM"); Class swizzledClass = [self class]; SEL originalSelector = @selector(setObject:forKey:); SEL swizzledSelector = @selector(safe_setObject:forKey:); Method originalMethod = class_getInstanceMethod(originalClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector); IMP originalIMP = method_getImplementation(originalMethod); IMP swizzledIMP = method_getImplementation(swizzledMethod); const char *originalType = method_getTypeEncoding(originalMethod); const char *swizzledType = method_getTypeEncoding(swizzledMethod); class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType); class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType); }
- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey { if (anObject && aKey) { [self safe_setObject:anObject forKey:aKey]; } else if (aKey) { [(NSMutableDictionary *)self removeObjectForKey:aKey]; } }
@end
|
4.在Method Swizzling之后如何恢復
使用了Method Swizzling的各種姿勢之后, 是否有考慮如何恢復到交換之前的現場呢?
一種方案就是通過一個開關標識符, 如果需要從邏輯上面恢復到交換之前, 就設置一下這個標識符, 在實現中判定如果設定了該標識符, 邏輯就直接調用原方法的實現, 其它什么事兒也不干, 這是目前大多數代碼的實現方法, 當然也是非常安全的方式, 只不過當交換方法過多時, 每一個交換的方法體中都需要增加這樣的邏輯, 並且也需要維護大量這些標識符變量, 只是會覺得不夠優雅, 所以此處也就不展開詳細討論了.
那下面來討論一下有沒有更好的方案, 以上描述的Method Swizzling各種場景和處理的技巧, 但綜合總結之后最核心的其實也只做了兩件事情:
- class_addMethod 添加一個新的方法, 可能是把其它類中實現的方法添加到目標類中, 也可能是把父類實現的方法添加一份在子類中, 可能是添加的實例方法, 也可能是添加的類方法, 總之就是添加了方法.
- 交換IMP 交換方法的實現IMP,完成這個步驟除了使用
method_exchangeImplementations
這個方法外, 也可以是調用了method_setImplementation
方法來單獨修改某個方法的IMP, 或者是采用在調用class_addMethod
方法中設定了IMP而直接就完成了IMP的交換, 總之就是對IMP的交換.
那我們來分別看一下這兩件事情是否都還能恢復:
- 對於
class_addMethod
, 我們首先想到的可能就是有沒有對應的remove方法呢, 在Objective-C 1.0的時候有class_removeMethods
這個方法, 不過在2.0的時候就已經被禁用了, 也就是蘋果並不推薦我們這樣做, 想想似乎也是挺有道理的, 本來runtime的接口看着就挺讓人心驚膽戰的, 又是添加又是刪除總覺得會出岔子, 所以只能放棄remove的想法, 反正方法添加在那兒倒也沒什么太大的影響.
- 針對IMP的交換, 在Method Swizzling時做的交換動作, 如果需要恢復其實要做的動作還是交換回來罷了, 所以是可以做到的, 不過需要怎樣做呢? 對於同一個類, 同一個方法, 可能會在不同的地方被多次做Method Swizzling, 所以要回退某一次的Method Swizzling, 我們就需要記錄下來這一次交換的時候是哪兩個IMP做了交換, 恢復的時候再換回來即可. 另一個問題是如果已經經過多次交換, 我們怎樣找到這兩個IMP所對應的Mehod呢, 還好runtime提供了一個
class_copyMethodList
方法, 可以直接取出Method列表, 然后我們就可以逐個遍歷找到IMP所對應的Method了, 下面是對上一個示例添加恢復之后實現的代碼邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
#import <objc/runtime.h>
@interface MySafeDictionary : NSObject @end
static NSLock *kMySafeLock = nil; static IMP kMySafeOriginalIMP = NULL; static IMP kMySafeSwizzledIMP = NULL;
@implementation MySafeDictionary
+ (void)swizzlling { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ kMySafeLock = [[NSLock alloc] init]; }); [kMySafeLock lock]; do { if (kMySafeOriginalIMP || kMySafeSwizzledIMP) break; Class originalClass = NSClassFromString(@"__NSDictionaryM"); if (!originalClass) break; Class swizzledClass = [self class]; SEL originalSelector = @selector(setObject:forKey:); SEL swizzledSelector = @selector(safe_setObject:forKey:); Method originalMethod = class_getInstanceMethod(originalClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector); if (!originalMethod || !swizzledMethod) break; IMP originalIMP = method_getImplementation(originalMethod); IMP swizzledIMP = method_getImplementation(swizzledMethod); const char *originalType = method_getTypeEncoding(originalMethod); const char *swizzledType = method_getTypeEncoding(swizzledMethod); kMySafeOriginalIMP = originalIMP; kMySafeSwizzledIMP = swizzledIMP; class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType); class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType); } while (NO); [kMySafeLock unlock]; }
+ (void)restore { [kMySafeLock lock]; do { if (!kMySafeOriginalIMP || !kMySafeSwizzledIMP) break; Class originalClass = NSClassFromString(@"__NSDictionaryM"); if (!originalClass) break; unsigned int outCount = 0; Method *methodList = class_copyMethodList(originalClass, &outCount); for (unsigned int idx=0; idx < outCount; idx++) { Method aMethod = methodList[idx]; IMP aIMP = method_getImplementation(aMethod); if (aIMP == kMySafeSwizzledIMP) { method_setImplementation(aMethod, kMySafeOriginalIMP); } else if (aIMP == kMySafeOriginalIMP) { method_setImplementation(aMethod, kMySafeSwizzledIMP); } } kMySafeOriginalIMP = NULL; kMySafeSwizzledIMP = NULL; } while (NO); [kMySafeLock unlock]; }
- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey { if (anObject && aKey) { [self safe_setObject:anObject forKey:aKey]; } else if (aKey) { [(NSMutableDictionary *)self removeObjectForKey:aKey]; } }
@end
|
注意 這段代碼的Method Swizzling和恢復都需要主動調用, 並且相比上面其它的示例, 這段代碼還添加如鎖機制來加之保護. 這個示例是以不同的類來實現的Method Swizzling和恢復, 如果是Category或者是類方法, 根據之前的示例也需要做相應的調整.