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

  

個人水平有限,如果本文由不足或者你有更好的想法,歡迎留言討論

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM