【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,例如在有些不能直接修改源码的地方

  

个人水平有限,如果本文由不足或者你有更好的想法,欢迎留言讨论

 

posted @ 2015-07-31 22:56  bomo  阅读(2004)  评论(1编辑  收藏  举报