埋點可以解決兩大類問題:一是了解用戶使用App的行為福贞,二是降低分析線上問題的難度。
iOS開發(fā)中常見的埋點方式停士,主要包括代碼埋點挖帘、可視化埋點和無埋點這三種。
運行時方法替換方式進行埋點
在iOS開發(fā)中最常見的三種埋點向瓷,就是對頁面進入次數(shù)、頁面停留時間舰涌、點擊事件的埋點猖任。
具體的實現(xiàn)方法是:先寫一個運行時方法替換的類SMHook,加上替換的方法 hookClass:fromSelector:toSelector瓷耙,代碼如下:
#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 返回成功表示被替換的方法沒實現(xiàn)朱躺,然后會通過 class_addMethod 方法先實現(xiàn);返回失敗則表示被替換方法已存在搁痛,可以直接進行 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
這個方法利用運行時 method_exchangeImplementations 接口將方法的實現(xiàn)進行了交換长搀,原方法調(diào)用時就會被 hook 住,從而去執(zhí)行指定的方法鸡典。
面進入次數(shù)源请、頁面停留時間都需要對 UIViewController 生命周期進行埋點,你可以創(chuàng)建一個 UIViewController 的 Category彻况,代碼如下:
@implementation UIViewController (logger)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 通過 @selector 獲得被替換和替換方法的 SEL谁尸,作為 SMHook:hookClass:fromeSelector:toSelector 的參數(shù)傳入
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 {
// 先執(zhí)行插入代碼,再執(zhí)行原 viewWillAppear 方法
[self insertToViewWillAppear];
[self hook_viewWillAppear:animated];
}
- (void)hook_viewWillDisappear:(BOOL)animated {
// 執(zhí)行插入代碼纽甘,再執(zhí)行原 viewWillDisappear 方法
[self insertToViewWillDisappear];
[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
那么良蛮,我們要怎么區(qū)別不同的 UIViewController 呢?我一般采取的做法都是悍赢,使用NSStringFromClass([self class]) 方法來取類名决瞳。這樣货徙,我就能夠通過類名來區(qū)別不同的UIViewController了。
對于點擊事件來說皮胡,我們也可以通過運行時方法替換的方式進行無侵入埋點痴颊。這里最主要的工作是,找到這個點擊事件的方法 sendAction:to:forEvent:胸囱,然后在 +load() 方法使用 SMHook 替換成為你定義的方法祷舀。完整代碼實現(xiàn)如下:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 通過 @selector 獲得被替換和替換方法的 SEL,作為 SMHook:hookClass:fromeSelector:toSelector 的參數(shù)傳入
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 在一個視圖類中可能有多個不同的繼承類裳扯,相同 UIButton 的子類在不同視圖類的埋點也要區(qū)別開。所以谤职,我們需要通過 “action 選擇器名 NSStringFromSelector(action)” +“視圖類名 NSStringFromClass([target class])”組合成一個唯一的標識饰豺,來進行埋點記錄。
事件唯一標識
每個子視圖在父視圖中都會有自己的索引允蜈,所以如果我們再加上這個索引的話冤吨,每個視圖的標識就是唯一的了。
UITableViewCell 需要使用 indexPath饶套,這個值里包含了 section 和 row 的值漩蟆。所以,我們可以通過 indexPath 來確定每個 Cell 的唯一性
除了上面提到的這些特殊情況外妓蛮,還有一種情況使得我們也難以得到準確的唯一標識怠李。如果視圖層級在運行時會被更改,比如執(zhí)行 insertSubView:atIndex:蛤克、removeFromSuperView 等方法時捺癞,我們也無法得到唯一標識,即使只截取部分路徑也無法保證后期代碼更新時不會動到這個部分构挤。就算是運行時視圖層級不會修改髓介,以后需求迭代頁面更新頻繁的話,視圖唯一標識也需要同步的更新維護筋现。
這種問題就不好解決了唐础,事件唯一標識的準確性難以保障,這也是通過運行時方法替換進行無侵入埋點很難在各個公司全面鋪開的原因矾飞。雖然無侵入埋點無法覆蓋到所有情況彻犁,全面鋪開面臨挑戰(zhàn),但是無侵入埋點還是解決了大部分的埋點需求凰慈,也節(jié)省了大量的人力成本汞幢。
小結(jié)
今天這篇文章,我與你分享了運行時替換方法進行無侵入埋點的方案微谓。這套方案由于唯一標識難以維護和準確性難以保障的原因森篷,很難被全面采用输钩,一般都只是用于一些功能和視圖穩(wěn)定的地方,手動侵入式埋點方式依然占據(jù)大部分場景仲智。
無侵入埋點也是業(yè)界一大難題买乃,目前還只是初級階段,還有很長的路要走钓辆。我認為剪验,運行時替換方法的方式也只是一種嘗試,但是現(xiàn)實中業(yè)務代碼太過復雜前联。同時功戚,為了使無侵入的埋點能夠覆蓋得更全、準確度更高似嗤,代價往往是對埋點所需的標識維護成本不斷增大啸臀。
所以說,我覺得這種方案并不一定是未來的方向烁落。我倒是覺得使用 Clang AST 的接口乘粒,在構(gòu)建時遍歷 AST,通過定義的規(guī)則將所需要的埋點代碼直接加進去伤塌,可能會更加合適灯萍。這時,我們可以使用前一篇文章“如何利用 Clang 為 App 提質(zhì)每聪?”中提到的 LibTooling 來開發(fā)一個獨立的工具旦棉,專門以靜態(tài)方式插入埋點代碼。這樣做熊痴,既可以享受到手動埋點的精確性他爸,還能夠享受到無侵入埋點方式的統(tǒng)一維護聂宾、開發(fā)解耦果善、易維護的優(yōu)勢。