目录
小菜
埋点解决两大问题
- 了解用户行为,为改进产品方案提供数据支持
- 根据用户行为降低分析线上问题的难度
埋点方式
- 代码埋点主要就是通过手写代码的方式来埋点,能很精确的在需要埋点的代码处加上埋点的代码,可以很方便地记录当前环境的变量值,方便调试,并跟踪埋点内容,但存在开发工作量大,并且埋点代码到处都是,后期难以维护等问题。(手动埋点,想埋哪就埋哪;但是消耗了开发成本,同时到处散落着埋点代码让代码不好看,也难以维护)
- 可视化埋点,就是将埋点增加和修改的工作可视化了,提升了增加和维护埋点的体验。(什么技术实现可视化埋点?)
- 无埋点,并不是不需要埋点,而更确切地说是“全埋点”,而且埋点代码不会出现在业务代码中,容易管理和维护。它的缺点在于,埋点成本高,后期的解析也比较复杂,再加上 view_path 的不确定性。所以,这种方案并不能解决所有的埋点需求,但对于大量通用的埋点需求来说,能够节省大量的开发和维护成本。
主菜-无侵入埋点实现方案
运行时方法替换(即HOOK方法)
话不多说直接上代码
交换方法类SMHook
#import "SMHook.h"
#import <objc/runtime.h>
@implementation SMHook
+ (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);
}
}
@end
页面进入次数、页面停留时间都需要对 UIViewController 生命周期进行埋点,通过类名来区别不同的 UIViewController。
代码如下:
@implementation UIViewController (logger)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 通过 @selector 获得被替换和替换方法的 SEL,作为 SMHook:hookClass:fromeSelector:toSelector 的参数传入
SEL fromSelectorAppear = @selector(viewWillAppear:);
SEL toSelectorAppear = @selector(hook_viewWillAppear:);
[SMHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
SEL fromSelectorDisappear = @selector(viewWillDisappear:);
SEL toSelectorDisappear = @selector(hook_viewWillDisappear:);
[SMHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
});
}
- (void)hook_viewWillAppear:(BOOL)animated {
// 先执行插入代码,再执行原 viewWillAppear 方法
[self insertToViewWillAppear];
// 注意这里因为方法指针已经被交换了,所以调用hook_viewWillAppear
[self hook_viewWillAppear:animated];
}
- (void)hook_viewWillDisappear:(BOOL)animated {
// 执行插入代码,再执行原 viewWillDisappear 方法
[self insertToViewWillDisappear];
// 注意这里因为方法指针已经被交换了,所以调用hook_viewWillDisappear
[self hook_viewWillDisappear:animated];
}
- (void)insertToViewWillAppear {
// 在 ViewWillAppear 时进行日志的埋点
[[[[SMLogger create]
message:[NSString stringWithFormat:@"%@ Appear",NSStringFromClass([self class])]]
classify:ProjectClassifyOperation]
save];
}
- (void)insertToViewWillDisappear {
// 在 ViewWillDisappear 时进行日志的埋点
[[[[SMLogger create]
message:[NSString stringWithFormat:@"%@ Disappear",NSStringFromClass([self class])]]
classify:ProjectClassifyOperation]
save];
}
@end
对于点击事件来说,我们也可以通过运行时方法替换的方式进行无侵入埋点。代码如下:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 通过 @selector 获得被替换和替换方法的 SEL,作为 SMHook:hookClass:fromeSelector:toSelector 的参数传入
SEL fromSelector = @selector(sendAction:to:forEvent:);
SEL toSelector = @selector(hook_sendAction:to:forEvent:);
[SMHook 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]);
[[[SMLogger create] message:[NSString stringWithFormat:@"%@ %@",targetName,actionString]] save];
}
}
除了 UIViewController、UIButton 控件以外,Cocoa 框架的其他控件都可以使用这种方法来进行无侵入埋点。以 Cocoa 框架中最复杂的 UITableView 控件为例,你可以使用 hook setDelegate 方法来实现无侵入埋点。另外,对于 Cocoa 框架中的手势事件(Gesture Event),我们也可以通过 hook initWithTarget:action: 方法来实现无侵入埋点。
总结
这套方案由于唯一标识难以维护和准确性难以保障的原因,很难被全面采用,一般都只是用于一些功能和视图稳定的地方,手动侵入式埋点方式依然占据大部分场景。
饭后甜点
Clang AST 实现无侵入埋点
使用 Clang AST 的接口,在构建时遍历 AST,通过定义的规则将所需要的埋点代码直接加进去,可能会更加合适。这时,我们可以使用前一篇文章“如何利用 Clang 为 App 提质?”中提到的 LibTooling 来开发一个独立的工具,专门以静态方式插入埋点代码。这样做,既可以享受到手动埋点的精确性,还能够享受到无侵入埋点方式的统一维护、开发解耦、易维护的优势。
Swift怎么实现运行时方法置换
参考swiftlint使用 & swift替换AppIcon & Swift替换方法 - 简书Swift之交换/替换方法 - Woodjobber
Swift
是一种强类型语言。即默认类型是安全的静态类型。纯Swift类的函数调用已经不再是OC的运行时发送消息,而是类似于C++的vtable,在编译时就确定了调用哪个函数,所以没法通过runtime获取方法,属性。Swift中的动态性可以通过OC运行时来获得,动态性最常用的就是方法替换(Method Swizzling)。
Swift中的动态修饰符
@objc
: 将`Swift 函数 暴露给 OC运行时,但是它仍然不能保证完全动态,编译器会尝试去对它做静态优化@dynamic
: 动态功能修饰符,它能保证函数,属性可以获得动态性
实现方式
1. 用Swift
中的unsafeBitCast
实现,代码如下:
extension SayHello {
class func swizzleSayHello() {
let originalSlector = #selector(SayHello.sayHello(_:))
let originalMethod = class_getInstanceMethod(SayHello.self, originalSlector)!
let originalImplementation = method_getImplementation(originalMethod)
//id (*IMP)(id, SEL, ...)
typealias originalClosureType = @convention(c) (AnyObject,Selector,String) -> Void
let originalClosure:originalClosureType = unsafeBitCast(originalImplementation, to: originalClosureType.self)
let newBlock:@convention(block)(AnyObject,String) -> Void = {
obj, name in
var name__ = name
name__ = name + ",nice to meet you"
return originalClosure(obj,originalSlector,name__)
}
//Its signature should be: method_return_type ^(id self, method_args...).
let newImplementation = imp_implementationWithBlock(unsafeBitCast(newBlock, to: AnyObject.self))
print(newImplementation)
method_setImplementation(originalMethod, newImplementation)
}
}
2.方法替换
extension SayHello {
//Argument of '#selector' refers to instance method 'swizzleSayHello' that is not exposed to Objective-C
//Add '@objc' to expose this instance method to Objective-C
//@objc 表示 暴露 方法给 oc 调用
@objc private func swizzledSayHello(_ name: String) -> Void {
var name__ = name
name__ = name + ",nice to meet you"
return swizzledSayHello(name__)
}
class func swizzledSayHello() {
let originalSelctor = #selector(SayHello.sayHello(_:))
let swizzledSelector = #selector(SayHello.swizzledSayHello(_:))
swizzleMethod(for: SayHello.self, originalSelector: originalSelctor, swizzledSelector: swizzledSelector)
}
private class func swizzleMethod(for aClass: AnyClass, originalSelector: Selector,swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(aClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector)
let didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
if didAddMethod {
class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
} else {
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}
}
在AppDelegate的启动方法里调用(显得有点low。。。)
3. 创建OC拓展文件, 将文件引入到混合开发桥接文件中。(多个拓展文件的+load()方法会被覆盖吗,+initialize()方法呢???)
参考:App 启动速度怎么做优化与监控?_学海无涯乐作舟的博客-CSDN博客
+load()调用情况如下(类或者分类被加载时调用):
1.先调用类的+load()方法,先编译(build phases的添加顺序)哪个就先调用哪个;
2.调用该类的+load()方法之前,先调用父类的+load()方法;
3.分类的按照编译先后顺序调用+load方法;
总结下来,就是独立制的。父类优先子类,类优先分类,类和分类之间按照谁先编译谁先调用。谁实现了就会按照顺序去调用谁,不会覆盖。
+initialize()调用情况如下(类被初始化时调用):
1.初始化时先调用父类,再调用子类;
2.如果子类未实现+initialize(),则调用父类;
3.分类的+initialize()会覆盖掉原来类的+initialize();
总结下来,是属于责任制的。父类优先子类调用,子类不行父类上,分类实现了本类闲。这里注意,如果多个分类都实现了,按照编译先后顺序,最后一个分类的实现覆盖前面的。
思考
如本类和类别都做了方法hook,而load中hook则能满足这种场景。放在load中注意不要影响启动时间,所以根据业务场景选择是在load还是在InInitialize中hook。
其实,也可以通过继承基类,在基类中埋点实现。不过这种方式对于代码是有侵入的,不够灵活。