【iOS】利用Runtime特性做监控
最近在看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
https://files.cnblogs.com/files/bomo/MonitorDemo.zip
6、总结
通过这个特性,我们可以用到监控和统计上,我们可以在相关的函数进行埋点,统计一个函数调用了多少次,请求成功率,失败日志的统计等,也可以在不改变原来代码的情况下修复一些bug,例如在有些不能直接修改源码的地方
个人水平有限,如果本文由不足或者你有更好的想法,欢迎留言讨论