在iOS开发中,埋点可以解决两大类问题:一是了解用户使用App的习惯,二是降低分析线上问题的难度。目前,iOS常见的埋点方式有三种:代码埋点、可视化埋点和无埋点这三种。
- 代码埋点主要就是通过手写代码的方式来埋点,能很精确的在需要埋点的代码处加上埋点的代码,可以很方便地记录当前环境的变量值,方便调试,并跟踪埋点内容,但存在开发工作量大,并且埋点代码到处都是,后期难以维护等问题。
- 可视化埋点,就是将埋点增加和修改的工作可视化了,提升了增加和维护埋点的体验。
- 无埋点,并不是不需要埋点,而更确切地说是“全埋点”,而且埋点代码不会出现在业务代码中,容易管理和维护。它的缺点在于,埋点成本高,后期的解析也比较复杂,再加上 view_path 的不确定性。所以,这种方案并不能解决所有的埋点需求,但对于大量通用的埋点需求来说,能够节省大量的开发和维护成本。
可视化埋点和无埋点,都属于是无侵入的埋点方案,因为它们都不需要在工程代码中写入埋点代码。所以,采用这样的无侵入埋点方案,既可以做到埋点被统一维护,又可以实现和工程代码的解耦。
对于iOS大多数常见埋点情况来说,我们都可以通过运行时方法替换技术来插入埋点代码,以实现无侵入埋点的埋点方法。
首先,写一个运行时方法替换的类 ***Hook。
+ (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
Class class = classObject;
// 得到被替换类的实例方法
Method fromMethod = class_getInstanceMethod(class, fromSelector);
// 得到替换类的实例方法
Method toMethod = class_getInstanceMethod(class, toSelector);
// class_addMethod 返回成功表示被替换的方法没实现,然后会通过 class_addMethod 方法先实现;返回失败则表示被替换方法已存在,可以直接进行 IMP 指针交换
if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
// 进行方法的替换
class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
} else {
// 交换 IMP 指针
method_exchangeImplementations(fromMethod, toMethod);
}
}
然后,再写个工具类,记录下统计信息(Hook了谁,做了什么)。
接下来就是在几个类中写替换方法了:
UIViewController
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 通过 @selector 获得被替换和替换方法的 SEL,作为 ***Hook:hookClass:fromeSelector:toSelector 的参数传入
SEL fromSelectorAppear = @selector(viewWillAppear:);
SEL toSelectorAppear = @selector(hook_viewWillAppear:);
[***Hook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
SEL fromSelectorDisappear = @selector(viewWillDisappear:);
SEL toSelectorDisappear = @selector(hook_viewWillDisappear:);
[***Hook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
});
}
- (void)hook_viewWillAppear:(BOOL)animated {
// 先执行插入代码,再执行原 viewWillAppear 方法
[self insertToViewWillAppear];
[self hook_viewWillAppear:animated];
}
- (void)hook_viewWillDisappear:(BOOL)animated {
// 执行插入代码,再执行原 viewWillDisappear 方法
[self insertToViewWillDisappear];
[self hook_viewWillDisappear:animated];
}
- (void)insertToViewWillAppear {
// 在 ViewWillAppear 时进行日志的埋点
// HookCode here
}
- (void)insertToViewWillDisappear {
// 在 ViewWillDisappear 时进行日志的埋点
// HookCode here
}
UIControl
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 通过 @selector 获得被替换和替换方法的 SEL,作为 ***Hook:hookClass:fromeSelector:toSelector 的参数传入
SEL fromSelector = @selector(sendAction:to:forEvent:);
SEL toSelector = @selector(hook_sendAction:to:forEvent:);
[***Hook hookClass:self fromSelector:fromSelector toSelector:toSelector];
});
}
- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[self insertToSendAction:action to:target forEvent:event];
[self hook_sendAction:action to:target forEvent:event];
}
- (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// 日志记录
if ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) {
NSString *actionString = NSStringFromSelector(action);
NSString *targetName = NSStringFromClass([target class]);
// HookCode here
}
}
UITableView
+ (void)load {
SEL fromSelector = @selector(setDelegate:);
SEL toSelector = @selector(fpc_toDelegate:);
[***Hook hookClass:self fromSelector:fromSelector toSelector:toSelector];
}
- (void)hook_toDelegate:(id <UITableViewDelegate>)delegate {
[self hook_toDelegate:delegate];
// 得到代理对象,代理对象会调用代理方法
SEL fromSelector = @selector(tableView:didSelectRowAtIndexPath:);
SEL toSelector = @selector(hook_tableView:didSelectRowAtIndexPath:);
// 得到被替换的类的实例方式
Method fromMethod = class_getInstanceMethod(delegate.class, fromSelector);
// 得到替换的类的实例方法
Method toMethod = class_getInstanceMethod(self.class, toSelector);
// class_addMethod 添加要替换的方法
class_addMethod(delegate.class, toSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod));
Method hookMethod = class_getInstanceMethod(delegate.class, toSelector);
method_exchangeImplementations(fromMethod, hookMethod);
}
-(void)fpc_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// HookCode here
[self hook_tableView:tableView didSelectRowAtIndexPath:indexPath];
}