09-无侵入埋点

一、埋点方式

  • 代码埋点,手写代码进行埋点。优点是追踪精确,方便记录当前环境的变量值,易于调试。缺点是工作量大,后期难以维护。
  • 无侵入埋点,在运行时通过替换方法实现无侵入埋点。优点是能节省大量开发和维护成本。缺点是不确定性,开发成本高,不能满足所有需求。

二、无侵入埋点实现方式

利用runtime特性,在运行时通过替换方法。

2.1 如何进行方法替换

我们写一个工具类,提供方法替换的接口,方法的实现如下:

+ (void)hookForClass:(Class)targetClass fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
    
    Method fromMethod = class_getInstanceMethod(targetClass, fromSelector);
    
    Method toMethod = class_getInstanceMethod(targetClass, toSelector);
    
    // 返回成功则表示被替换的方法没有实现,先添加实现。返回失败则表示已实现,直接进行IMP指针交换
    if (class_addMethod(targetClass, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        // 进行方法替换
        class_replaceMethod(targetClass, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
    }else {
        // 交换IMP指针
        method_exchangeImplementations(fromMethod, toMethod);
    }
}

2.2 如何进行hook

以UIViewController的viewWillAppear和viewWillDisappear方法为例,建一个UIViewController的基类,让项目中用到的controller都继承自它,或者使用UIViewController的分类。这里我使用的前一种方法,实现代码如下:

#import "JCBaseViewController.h"
#import "JCHook.h"

@implementation JCBaseViewController

#pragma mark - initialize
+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [JCHook hookForClass:self fromSelector:@selector(viewWillAppear:) toSelector:@selector(hook_viewWillAppear:)];
        [JCHook hookForClass:self fromSelector:@selector(viewWillDisappear:) toSelector:@selector(hook_viewWillDisappear:)];
    });
}

#pragma mark - hook method
- (void)hook_viewWillAppear:(BOOL)animated {
    // 埋点代码
    [self insertViewWillAppear];
    // 调用原方法
    [self hook_viewWillAppear:animated];
}

- (void)hook_viewWillDisappear:(BOOL)animated {
    [self insertViewWillDisappear];
    [self hook_viewWillDisappear:animated];
}

#pragma mark - private Methods
- (void)insertViewWillAppear {
    NSLog(@"%@ && %s",NSStringFromClass([self class]),__func__);
}

- (void)insertViewWillDisappear {
    NSLog(@"%@ && %s",NSStringFromClass([self class]),__func__);
}

@end

这里有两个地方需要注意一下。

  • 其实在initialize和load里都可以进行hook,之所以选用initialize是因为load方法是在main函数执行之前调用,会增加程序启动时间。
  • hook_viewWillAppear中我们又调用了hook_viewWillAppear,这里不会造成递归调用。原因是当我们手动调用hook_viewWillAppear时,其SEL对应的IMP已经指向了原有的方法viewWillAppear,所以实际上是执行原有viewWillAppear的IMP。

三、课后作业

实现 UITableViewCell 点击事件的无侵入埋点。

上面我们实现了对UIViewController的生命周期进行埋点,相对来说较为容易,因为方法的调用者是它本身。UITableViewCell的点击方法调用对象则是它的delegate,那么我们如何进行hook呢?

既然我们无法直接hook点击方法,那么我们就需要尝试hook点击方法之前的方法。下面就一步一步分析如何hook前一步方法。

3.1 找到点击的代理方法之前的方法

我们先写一个简单的UITableView并在其点击的代理方法中打一个断点,看看程序调用堆栈。如图:


堆栈信息

我们注意到在调用tableView:didSelectRowAtIndexPath:之前,tableView调用了一个名为_selectRowAtINdexPath:animated:scrollPosition:notifyDelegate:方法。接下来尝试hook这个方法。

3.2 解析目标方法

我们发现目标方法并没有暴露在tableView的头文件中,所以我们无法直接知道目标方法的参数类型、返回值等等信息。接下来就先进行方法解析。
分析方法的代码实现如下:

+ (void)analysisMethod:(Method)method {
    // 获取方法的参数类型
    unsigned int argumentsCount = method_getNumberOfArguments(method);
    char argName[512] = {};
    for (unsigned int j = 0; j < argumentsCount; ++j) {
        method_getArgumentType(method, j, argName, 512);
        
        NSLog(@"第%u个参数类型为:%s", j, argName);
        memset(argName, '\0', strlen(argName));
    }
    
    char returnType[512] = {};
    method_getReturnType(method, returnType, 512);
    NSLog(@"返回值类型:%s", returnType);
    
    // type encoding
    NSLog(@"TypeEncoding: %s", method_getTypeEncoding(method));
}

调用方法如下:

 Method method = class_getInstanceMethod(self, @selector(_selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:));
[self analysisMethod:method];

控制台输出如下:

2019-05-06 21:52:58.836494+0800 09-无侵入埋点[19358:8173279] 第0个参数类型为:@
2019-05-06 21:52:58.836706+0800 09-无侵入埋点[19358:8173279] 第1个参数类型为::
2019-05-06 21:52:58.836856+0800 09-无侵入埋点[19358:8173279] 第2个参数类型为:@
2019-05-06 21:52:58.836966+0800 09-无侵入埋点[19358:8173279] 第3个参数类型为:B
2019-05-06 21:52:58.837076+0800 09-无侵入埋点[19358:8173279] 第4个参数类型为:q
2019-05-06 21:52:58.837175+0800 09-无侵入埋点[19358:8173279] 第5个参数类型为:B
2019-05-06 21:52:58.837277+0800 09-无侵入埋点[19358:8173279] 返回值类型:v
2019-05-06 21:52:58.837377+0800 09-无侵入埋点[19358:8173279] TypeEncoding: v40@0:8@16B24q28B36

前面两个参数我们可以不用关心,因为在方法调用时代码会被编译成类似这个样子:

((void (*)(id, SEL))objc_msgSend)((id)m, @selector(selectorName));

我们看到后面的四个参数类型分别为@、B、q、B,这些又是什么呢?
下面是官方的Type Encoding对应表
[站外图片上传中...(image-7b92da-1557152518714)]

根据Type Encoding对应表我们知道@ 表示 id对象,B表示Bool,q表示long long,以及返回值v表示void。
那么,我们的目标函数应该是这个样子:

- (void)hook_selectRowAtIndexPath:(id)indexPath animated:(BOOL)animated scrollPosition:(long long)scrollPosition notifyDelegate:(BOOL)notifyDelegate

再然后我们发现tableVIew头文件中有这个方法:

- (void)selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition

那么我们的目标函数其实可以写成这个样子:

- (void)hook_selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition notifyDelegate:(BOOL)notifyDelegate

通过打印id对象也能知道其具体类型。到这里我们就已经找到并解析出需要进行hook的目标方法了。

3.3 对目标方法进行hook

同样的,创建tableView的基类或者分类来进行hook。具体代码的核心实现如下:

#pragma mark - initialize
+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method method = class_getInstanceMethod(self, @selector(_selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:));
        [self analysisMethod:method];
        
        [JCHook hookForClass:self fromSelector:@selector(_selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:) toSelector:@selector(hook_selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:)];
    });
}
#pragma mark - hook method

- (void)hook_selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition notifyDelegate:(BOOL)notifyDelegate {
    [self insertTableViewDidSelectIndexPath:indexPath];
    [self hook_selectRowAtIndexPath:indexPath animated:animated scrollPosition:scrollPosition notifyDelegate:notifyDelegate];
}

#pragma mark - private Methods
- (void)insertTableViewDidSelectIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"%@",indexPath);
}

最后附上完整代码

更多详细内容,请移步至戴铭老师的专栏

你可能感兴趣的:(09-无侵入埋点)