最近在看Object-C運行時特性,其中有一個特別好用的特性叫 Method Swizzling ,可以動態交換函數地址,在應用程序加載的時候,通過運行時特性互換兩個函數的地址,不改變原有代碼而改變原有行為,達到偷天換日的效果,下面直接看效果吧
1、我們先創建一個Calculator類,並提供兩個簡單的方法
#import <Foundation/Foundation.h> @interface Calculator : NSObject + (instancetype)shareInstance; - (NSInteger)addA:(NSInteger)a withB:(NSInteger)b; - (void)doSomethingWithParam:(NSString *)param success:(void (^)(NSString *result))success failure:(void (^)(NSString *error))failure; @end @implementation Calculator + (instancetype)shareInstance { static id instance = nil; static dispatch_once_t token; dispatch_once(&token, ^{ instance = [[self alloc] init]; }); return instance; } - (NSInteger)addA:(NSInteger)a withB:(NSInteger)b { return a + b; } - (void)doSomethingWithParam:(NSString *)param success:(void (^)(NSString *result))success failure:(void (^)(NSString *error))failure { //TODO: do some things, //simulating result BOOL result = arc4random() % 2 == 1; if (result) { success(@"success"); } else { failure(@"error"); } } @end
2、接下來我們在ViewController測試一下
#import "ViewController.h" #import "Calculator.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Calculator *calculator = [Calculator shareInstance]; NSInteger addResult = [calculator addA:2 withB:3]; NSLog(@"calculate result: %ld", addResult); [calculator doSomethingWithParam:@"param" success:^(NSString *result) { NSLog(@"doSomething %@", result); } failure:^(NSString *error) { NSLog(@"doSomethime %@", error); }]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end
3、兩個函數執行后,輸出結果如下
4、現在我們有一個需求,在這這兩個函數執行的前后在控制台輸出執行信息
在 doSomethingWithParam:success:failure: 執行成功或失敗的時候也輸出信息,在不修改原有代碼的情況下,我們可以根據Runtime的API自定義一個新的函數,然后再執行原函數前后輸出信息
4.1、我們先創建一個工具類 SGRumtimeTool 用於交換函數
#import <Foundation/Foundation.h> #import <objc/runtime.h> @interface SGRumtimeTool : NSObject + (void)changeMethodWithClass:(Class)class oldMethod:(SEL)oldMethod newMethod:(SEL)newMethod; @end @implementation SGRumtimeTool + (void)changeMethodWithClass:(Class)class oldMethod:(SEL)oldMethod newMethod:(SEL)newMethod { Method originalMethod = class_getInstanceMethod(class, oldMethod); Method swizzledMethod = class_getInstanceMethod(class, newMethod); BOOL didAddMethod = class_addMethod(class, oldMethod, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, oldMethod, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } @end
4.2、通過分類的方式,定義新函數,同時在初始化時互換方法(load)
注:NSObject 提供了兩個靜態的初始化方法 initialize 和 load,load在應用程序啟動后就會執行,而initialize在類被第一次使用的時候執行,關於 load 和initialize 的區別的詳細分析,參見:http://www.cnblogs.com/ider/archive/2012/09/29/objective_c_load_vs_initialize.html
推薦大家看一下上面的文章
下面我們定義 Calculator 的擴展分類
#import "Calculator.h" #import "SGRumtimeTool.h" @interface Calculator (Monitor) @end @implementation Calculator (Monitor) + (void)load { SEL oldAddMethod = @selector(addA:withB:); SEL newAddMethod = @selector(newAddA:withB:); [SGRumtimeTool changeMethodWithClass:[self class] oldMethod:oldAddMethod newMethod:newAddMethod]; SEL oldSomeMethod = @selector(doSomethingWithParam:success:failure:); SEL newSomeMethod = @selector(newDoSomethingWithParam:success:failure:); [SGRumtimeTool changeMethodWithClass:[self class] oldMethod:oldSomeMethod newMethod:newSomeMethod]; } /** * log some info before and after the method */ - (NSInteger)newAddA:(NSInteger)a withB:(NSInteger)b { NSLog(@"-------------- executing addA:withB: --------------"); //two method has swapped, call (newAddA:withB) will execute (addA:withB) NSInteger result = [self newAddA:a withB:b]; NSLog(@"-------------- executed addA:withB: --------------"); return result; } /** * log some info for the result */ - (void)newDoSomethingWithParam:(NSString *)param success:(void (^)(NSString *result))success failure:(void (^)(NSString *error))failure { NSLog(@"-------------- executing doSomethingWithParam:success:failure: --------------"); [self newDoSomethingWithParam:param success:^(NSString *result) { success(result); NSLog(@"-------------- execute success --------------"); } failure:^(NSString *error) { failure(error); NSLog(@"-------------- execute failure --------------"); }]; } @end
在Calculator (Monitor) 中,我們定義兩個新方法,並添加了一些輸出信息,當然我們可以根據我們的信息任意的修改該方法,調用的地方不變
上面方法看起來像遞歸調用,進入死循環了,但由於新方法與原來的方法進行了互換,所以我們在新函數調用原來的方法的時候需要使用新的方法名,不會死循環
4.3、調用的地方不變,運行一下看結果
原來所有的代碼都不變,我們只是新增了一個 Calculator (Monitor) 分類而已
5、Demo
http://files.cnblogs.com/files/bomo/MonitorDemo.zip
6、總結
通過這個特性,我們可以用到監控和統計上,我們可以在相關的函數進行埋點,統計一個函數調用了多少次,請求成功率,失敗日志的統計等,也可以在不改變原來代碼的情況下修復一些bug,例如在有些不能直接修改源碼的地方
個人水平有限,如果本文由不足或者你有更好的想法,歡迎留言討論