因為Objective-C的runtime機制, Method Swizzling這個黑魔法解決了我們實際開發中諸多常規手段所無法解決的問題, 比如代碼的插樁,Hook,Patch等等. 我們首先看看常規的Method Swizzling是怎樣用的, NSHipster有一篇介紹基本用法的文章Method Swizzling, 我們就先以這篇文章中的示例開始說起吧:
#import @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中的代碼了.
其實以上的代碼也可以簡寫為以下:
+ (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,swizzledSelector,originalIMP,originalType); class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType); }
這是因為class_replaceMethod方法其實能夠覆蓋到class_addMethod和method_setImplementation兩種場景, 對於第一個class_replaceMethod來說, 如果viewWillAppear:實現在父類, 則執行class_addMethod, 否則就執行method_setImplementation將原方法的IMP指定新的代碼塊; 而第二個class_replaceMethod完成的工作便只是將新方法的IMP指向原來的代碼.
但此處需要特別注意交換的順序,應該優化把新的方法指定原IMP,再修改原有的方法的IMP.
除了以上的場景之外,其它場景下我們如何使用Method Swizzling呢?
1.在不同類之間實現Method Swizzling
上面示例是通過Category來新增一個方法然後實現Method Swizzling的, 但有一些場景可能並不適合使用Category(比如私有的類,未獲取到該類的聲明), 此時我們應該如何來做Method Swizzling呢?
例如已知一個className為Car的類中有一個實例方法- (void)run:(double)speed, 目前需要Hook該方法對速度小於120才執行run的代碼, 按照方法交換的流程, 代碼應該是這樣的:
#import @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); // 向Car類中新添加一個xxx_run:的方法 BOOL registerMethod = class_addMethod(originalClass, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (!registerMethod) { return; } // 需要更新swizzledMethod變量,獲取當前Car類中xxx_run:的Method指針 swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector); if (!swizzledMethod) { return; } // 後續流程與之前的一致 BOOL didAddMethod = class_addMethod(originalClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(originalClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } - (void)xxx_run:(double)speed { if (speed < 120) { [self xxx_run:speed]; } } @end
與之前的流程相比,在前面添加了兩個邏輯:
利用runtime向目標類Car動態添加了一個新的方法,此時Car類與MyCar類一樣具備了xxx_run:這個方法,MyCar的利用價值便結束了;
為了完成後續Car類中run:與xxx_run:的方法交換,此時需要更新swizzledMethod變量為Car中的xxx_run:方法所對應的Method.
以上所有的邏輯也可以合並簡化為以下:
+ (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直接在Car類中注冊了xxx_run:方法, 並且指定的IMP為當前run:方法的IMP;
第二個class_replaceMethod與之前的邏輯一致, 當run:方法是實現在Car類或Car的父類, 分別執行method_setImplementation或class_addMethod;
2.如何實現類方法的Method Swizzling
以上的代碼都是實現的對實例方法的交換, 那如何來實現對類方法的交換呢, 依舊直接貼代碼吧:
@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,swizzledSelector,originalIMP,originalType); class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType); } + (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為空的不會拋出異常:
@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)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了, 下面是對上一個示例添加恢復之後實現的代碼邏輯:
#import @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; Method originalMethod = NULL; Method swizzledMethod = NULL; 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) { originalMethod = aMethod; } else if (aIMP == kMySafeOriginalIMP) { swizzledMethod = aMethod; } } // 盡可能使用exchange,因為它是atomic的 if (originalMethod && swizzledMethod) { method_exchangeImplementations(originalMethod, swizzledMethod); } else if (originalMethod) { method_setImplementation(originalMethod, kMySafeOriginalIMP); } else if (swizzledMethod) { method_setImplementation(swizzledMethod, kMySafeSwizzledIMP); } kMySafeOriginalIMP = NULL; kMySafeSwizzledIMP = NULL; } while (NO); [kMySafeLock unlock]; } - (void)safe_setObject:(id)anObject forKey:(id)aKey { if (anObject && aKey) { [self safe_setObject:anObject forKey:aKey]; } else if (aKey) { [(NSMutableDictionary *)self removeObjectForKey:aKey]; } } @end
注意 這段代碼的Method Swizzling和恢復都需要主動調用, 並且相比上面其它的示例, 這段代碼還添加如鎖機制來加之保護. 這個示例是以不同的類來實現的Method Swizzling和恢復, 如果是Category或者是類方法, 根據之前的示例也需要做相應的調整.