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);
}
最后附上完整代码
更多详细内容,请移步至戴铭老师的专栏