• 【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

      http://files.cnblogs.com/files/bomo/MonitorDemo.zip 

    6、总结

      通过这个特性,我们可以用到监控和统计上,我们可以在相关的函数进行埋点,统计一个函数调用了多少次,请求成功率,失败日志的统计等,也可以在不改变原来代码的情况下修复一些bug,例如在有些不能直接修改源码的地方

      

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

  • 相关阅读:
    centos7安装 mysqlclient 报错的解决办法
    linux yum配置代理
    mysql 基础知识
    centos7 安装MySQL
    win安装mysql
    centos7 安装Mariadb
    python socket
    python 协程
    python 线程
    python 进程
  • 原文地址:https://www.cnblogs.com/bomo/p/4693363.html
Copyright © 2020-2023  润新知