輕量級非侵入式埋點方案
在發展日新月異的移動網際網路時代,資料扮演著極其重要的角色。埋點作為一種最簡單最直接的使用者行為統計方式,能夠全面精確的採集使用者的使用習慣以及各功能點的迭代反饋等等,有了這些資料才能更好的驅動產品的決策設計和新業務場景的規劃。本文旨在提出一種輕量級非侵入式的埋點方案,其主要有以下三方面優勢
- 支援動態下發埋點配置
- 物理隔離埋點程式碼和業務程式碼
- 外掛式的埋點功能實現
該方案透過維護一個JSON
檔案來指定埋點所在的類和方法,繼而利用AOP
的方式在對應的類和方法執行時動態嵌入埋點程式碼。對於需要邏輯判斷來確定埋點值的場景,提供hook
方法的入參,以及所在類的屬性值讀取,根據相應的狀態值設定不同的埋點
埋點配置
埋點配置JSON
表中包含需要hook
的類名class
和具體的事件event
資訊,event
中包括hook
的方法和對應的埋點值。如下所示
{
"version": "0.1.0",
"tracking": [
{
"class": "RJMainViewController",
"event": {
"rj_main_tracking": [
"tripTypeViewChangedWithIndex:",
"tripLabClickWithLabKey:"
],
"user_fp_slide_click": "clickNavLeftBtn",
"user_fp_reflocate_click": "clickLocationBtn"
}
},
{
"class": "RJTripHistoryViewModel",
"event": {
"user_mytrip_show": "tableView:didSelectRowAtIndexPath:"
}
},
{
"class": "RJTripViewController",
"event": {
"rj_trip_tracking": "callServiceEvent"
}
}
]
}
簡單來說就是本來埋點需要手動在該方法寫入埋點程式碼來記錄埋點值,現在透過AOP
的方式物理隔離埋點程式碼和業務程式碼,避免埋點的邏輯侵入汙染業務邏輯。埋點包括固定埋點和需要邏輯判斷的場景化埋點,固定埋點如下所示
{
"class": "RJTripHistoryViewModel",
"event": {
"user_mytrip_show": "tableView:didSelectRowAtIndexPath:"
}
}
RJTripHistoryViewModel
為類名,tableView:didSelectRowAtIndexPath:
為需要hook
的該類中的方法,而user_mytrip_show
則是具體的埋點值,也就是當RJTripHistoryViewModel
中的tableView:didSelectRowAtIndexPath:
方法執行的時候記錄埋點值user_mytrip_show
{
"class": "RJTripViewController",
"event": {
"rj_trip_tracking": "callServiceEvent"
}
},
對於場景化埋點,則需要提供一個impl
類來提供相應的邏輯判斷。比如上述配置表中的rj_trip_tracking
為場景埋點的實現類,在該類中根據狀態量返回對應的埋點值,即當callServiceEvent
方法執行時會去找rj_trip_tracking
這個埋點impl
同名類,取該類返回的埋點值記錄埋點。需要注意到是event
中的key
值既可以作為埋點值也可以作為impl
的類名,埋點庫會首先判斷是否存在對應的類,存在即認為是impl
實現類,從該類中取具體的埋點值。反之,則認為是固定埋點值
配置表中的類名和方法名需要對應,在
hook
的時候會去匹配,如果發現類中不存在對應的方法,則會自動觸發斷言
固定埋點
對於固定的埋點,只需要在對應的方法執行時直接記錄埋點,利用來hook
指定的類和方法,程式碼如下所示
[class aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
[events enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) {
NSLog(@"<RJEventTracking> - %@", ename);
}];
} error:&error];
為了便於檢測無效的埋點,還需對hook
的類和方法進行匹配校驗,若類中沒有對應的方法,則丟擲斷言
+ (void)checkValidWithClass:(NSString *)class method:(NSString *)method {
SEL sel = NSSelectorFromString(method);
Class c = NSClassFromString(class);
BOOL respond = [c respondsToSelector:sel] || [c instancesRespondToSelector:sel];
NSString *err = [NSString stringWithFormat:@"<RJEventTracking> - no specified method: %@ found on class: %@, please check", method, class];
NSAssert(respond, err);
}
場景埋點
場景化埋點主要為同一事件但是在多種狀態或邏輯下不同埋點的情況,比如同是聯絡客服的操作,在各種訂單型別以及訂單狀態下所設定的埋點是不同的。這個情況下,埋點庫透過提供一個protocol
由埋點impl
類來實現,根據不同的邏輯判斷,返回對應的埋點值
@protocol RJEventTracking <NSObject>
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments;
@end
比如上文的rj_trip_tracking
類需要遵循RJEventTracking
協議,並根據相關邏輯判斷返回對應的埋點值
埋點實現類的類名需要與埋點配置
JSON
中的event
裡的key
保持一致,因為埋點庫會透過檢測是否有同名的類來實現外掛式的埋點規則。另外,一個impl
可以對應多個method
方法
狀態判斷
根據狀態量來確定埋點值。還是聯絡客服埋點的例子,根據訂單種類和訂單狀態來返回對應的埋點值,首先定義JSON
表中同名的impl
類,並遵循RJEventTracking
協議
#import "RJEventTracking.h"
NS_ASSUME_NONNULL_BEGIN
@interface rj_trip_tracking : NSObject <RJEventTracking>
@end
NS_ASSUME_NONNULL_END
在.m檔案中實現自定義埋點的協議方法trackingMethod:instance:arguments:
#import "rj_trip_tracking.h"
@implementation rj_trip_tracking
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
id dataManager = [instance property:@"dataManager"];
NSInteger orderStatus = [[dataManager property:@"orderStatus"] integerValue];
NSInteger orderType = [[dataManager property:@"orderType"] integerValue];
if ([method isEqualToString:@"callServiceEvent"]) {
if (orderType == 1) {
if (orderStatus == 1) {
return @"user_inbook_psgservice_click";
} else if (orderStatus == 2) {
return @"user_finishbook_psgservice_click";
}
} else {
return @"user_psgservice_click";
}
}
return nil;
}
@end
在協議方法中,可以獲取當前的例項(在這個示例下為RJTripViewController
)和入引數組。訂單的型別和狀態是儲存在RJTripViewController
中的dataManager
屬性中的,所以可以透過埋點庫封裝好的property:
方法來獲取屬性值,並根據屬性值返回對應的埋點名稱
@interface NSObject (RJEventTracking)
- (id)property:(NSString *)property;
@end
屬性值讀取的實現為
- (id)property:(NSString *)property {
return [NSObject runMethodWithObject:self selector:property arguments:nil];
}
其中的原理很簡單,就是將getter
方法封裝到NSInvocation
中並invoke
讀取返回值即可
+ (id)runMethodWithObject:(id)object selector:(NSString *)selector arguments:(NSArray *)arguments {
if (!object) return nil;
if (arguments && [arguments isKindOfClass:NSArray.class] == NO) {
arguments = @[arguments];
}
SEL sel = NSSelectorFromString(selector);
NSMethodSignature *signature = [object methodSignatureForSelector:sel];
if (!signature) {
return nil;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = sel;
invocation.arguments = arguments;
[invocation invokeWithTarget:object];
return invocation.returnValue_obj;
}
入參判斷
需要根據JSON
中設定的所hook
方法的入參來確定埋點名稱的情況。比如在訂單列表中點選全部,進行中,待支付,待評價,已完成等選單項時分別埋點。被hook
的方法為tripLabClickWithLabKey:
其引數為UILabel
,原先程式碼中透過Label
的tag
判斷是點選的哪個子項,同樣,我們也可以獲取到Label
的入參然後據此判斷。由於引數只有一個,所以可以直接取arguments
第一個值
#import "rj_main_tracking.h"
#import <UIKit/UIKit.h>
static NSString *order_types[5] = { @"user_order_all_click", @"user_order_ongoing_click",
@"user_order_unpay_click", @"user_order_unmark_click",
@"user_order_finish_click" };
@implementation rj_main_tracking
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
if ([method isEqualToString:@"tripLabClickWithLabKey:"]) {
UILabel *label = arguments[0];
if (!label || label.tag > 4) {
return nil;
}
return order_types[label.tag];
} else if ([method isEqualToString:@"tripTypeViewChangedWithIndex:"]) {
return @"xx_ryan_jin";
}
}
@end
透過AOP
來hook
方法時,可以獲取到當前hook
方法所對應的例項物件和入參,在呼叫協議方法時,直接傳給協議實現類
方法呼叫
和讀取屬性值類似,也是在不同場景下同一事件不同埋點名稱的情況,但獲取的狀態量不是當前例項物件的,而是某個方法的返回值,這種情況下可以透過埋點庫提供的方法呼叫函式來實現
@interface NSObject (RJEventTracking)
- (id)performSelector:(NSString *)selector arguments:(nullable NSArray *)arguments;
@end
比如獲取某個頁面的檢視型別,而這個檢視型別儲存於單例物件中
[RJViewTypeModel sharedInstance].viewType
該場景下則根據viewType的型別,來返回相應的埋點名稱
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
NSString *labKey = [instance property:@"labKey"];
id viewTypeModel = [NSClassFromString(@"RJViewTypeModel") performSelector:@"sharedInstance"
arguments:nil];
NSInteger viewType = [[viewTypeModel property:@"viewType"] integerValue];
if (viewType == 0) {
if ([labKey isEqualToString:@"rj_view_begin_add"]) {
return @"user_fp_book_on_click";
}
if ([labKey isEqualToString:@"rj_view_end_add"]) {
return @"user_fp_book_off_click";
}
}
if (viewType == 1) {
if ([labKey isEqualToString:@"rj_view_begin_add"]) {
return @"user_fr_on_click";
}
if ([labKey isEqualToString:@"rj_view_end_add"]) {
return @"user_fr_off_click";
}
}
return nil;
}
邏輯判斷
需要額外新增邏輯判斷的場景,比如在訂單詳情頁需要統計使用者進入頁面的檢視行為,但是詳情頁的型別需要在網路請求後才能獲取,而且該網路請求會定時觸發,所以埋點hook
的方法會走多次,該情況下,需要新增一個屬性用來標記是否已記錄埋點 。故而埋點庫需要提供動態新增屬性的功能
@interface NSObject (RJEventTracking)
- (id)extraProperty:(NSString *)property;
- (void)addExtraProperty:(NSString *)property defaultValue:(id)value;
@end
在埋點實現impl
類裡面,新增額外的屬性來標記是否已記錄過埋點
@implementation user_orderdetail_show
- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
if ([instance extraProperty:@"isRecorded"]) {
return nil;
}
[instance addExtraProperty:@"isRecorded" defaultValue:@(YES)];
return @"user_orderdetail_show";
}
@end
使用addExtraProperty:defaultValue:
來給當前例項動態新增屬性,而extraProperty:
方法則用來獲取例項的某個額外屬性。如果isRecorded
返回YES
代表已經記錄過該埋點,返回nil
值來忽略該次埋點
上面示例中新增的isRecorded屬性是因為埋點的需求,和業務邏輯無關,所以比較合理的方式是在埋點的外掛
impl
類中新增,避免影響業務程式碼
埋點庫動態新增屬性的原理也很簡單,利用runtime
的objc_setAssociatedObject
和objc_getAssociatedObject
方法來繫結屬性到例項物件
- (id)extraProperty:(NSString *)property {
return objc_getAssociatedObject(self, NSSelectorFromString(property));
}
- (void)addExtraProperty:(NSString *)property defaultValue:(id)value {
objc_setAssociatedObject(self, NSSelectorFromString(property), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
動態下發
埋點JSON
配置表可以由伺服器提供介面,客戶端在每次啟動時透過介面獲取最新埋點配置表,從而達到動態下發的目的,客戶端拿到JSON
後,讀取埋點資訊並生效
[RJEventTracking loadConfiguration:[[NSBundle mainBundle] pathForResource:@"RJUserTracking" ofType:@"json"]];
讀取的程式碼如下所示,主要邏輯為遍歷埋點中的類和hook
的方法,並檢測是固定埋點還是場景化埋點,對於場景化埋點的情況查詢是否有對應的埋點impl
實現類。當然,還需檢測JSON
配置表的合法性,每個類和其中的方法是否匹配
+ (void)loadConfiguration:(NSString *)path {
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
return;
}
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
NSString *version = dict[@"version"];
NSArray *ts = dict[@"tracking"];
[ts enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) {
Class class = NSClassFromString(obj[@"class"]);
NSDictionary *ed = obj[@"event"];
NSMutableDictionary *td = [NSMutableDictionary dictionaryWithCapacity:0];
[ed enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
NSMutableArray *tArr = [NSMutableArray arrayWithCapacity:0];
[tArr addObjectsFromArray:[obj isKindOfClass:[NSArray class]] ? obj : @[obj]];
[tArr enumerateObjectsUsingBlock:^(NSString *m, NSUInteger idx, BOOL *stop) {
if ([td.allKeys containsObject:m]) {
NSMutableArray *ms = [td[m] mutableCopy];
if (![ms containsObject:key]) [ms addObject:key];
td[m] = ms;
} else {
td[m] = @[key];
}
}];
}];
[td enumerateKeysAndObjectsUsingBlock:^(NSString *kmethod, NSArray <NSString *> *tArr, BOOL *stop) {
SEL sel = NSSelectorFromString(kmethod);
NSError *error = nil;
[self checkValidWithClass:obj[@"class"] method:kmethod];
[class aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
[tArr enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) {
NSString *ename = name;
id<RJEventTracking> t = [NSClassFromString(name) new];
if (t && [t respondsToSelector:@selector(trackingMethod:instance:arguments:)]) {
ename = [t trackingMethod:kmethod instance:info.instance
arguments:info.arguments];
}
if ([ename length]) {
NSLog(@"<RJEventTracking> - %@", ename);
}
}];
} error:&error];
[self checkHookStatusWithClass:obj[@"class"] method:kmethod error:error];
}];
}];
}
最後附上原始碼地址:
pod 'RJEventTracking'
在使用過程中有遇到什麼問題或者最佳化建議歡迎留言PR, 謝謝。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2471/viewspace-2823831/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- js無侵入埋點方案JS
- 一個輕量級react埋點元件React元件
- mockjs 實現前端非侵入式 mock 解決方案MockJS前端
- 前端埋點方案分析前端
- iOS 輕量級 HTML 解析方案iOSHTML
- 一個非侵入式跟蹤分析程式
- 輕量級日誌收集方案LokiLoki
- Android輕量級事件通訊方案Android事件
- 前端埋點統計方案思考前端
- 從Fresco原始碼中找到非侵入式的答案原始碼
- Android全量埋點實踐Android
- .NET無侵入式物件池解決方案物件
- vue宣告式埋點實踐Vue
- 非侵入式入侵 —— Web快取汙染與請求走私Web快取
- FastHook——巧妙利用動態代理實現非侵入式AOPASTHook
- Tideways、xhprof 和 xhgui 打造 PHP 非侵入式監控平臺IDEGUIPHP
- 使用phpAnalysis打造PHP應用非侵入式效能分析器PHP
- 埋點
- SpringBoot接入輕量級分散式日誌框架(GrayLog)Spring Boot分散式框架
- 前端監控和前端埋點方案設計前端
- Vue 專案宣告式主動埋點Vue
- 輕量級超級 css 工具CSS
- iOS全埋點解決方案-UITableView和UICollectionView點選事件iOSUIView事件
- iOS全埋點解決方案-控制元件點選事件iOS控制元件事件
- TDengine 助力西門子輕量級數字化解決方案
- Golang wails2 輕量級跨端桌面解決方案GolangAI跨端
- iOS全埋點解決方案-採集奔潰iOS
- iOS全埋點解決方案-資料儲存iOS
- iOS全埋點解決方案-時間相關iOS
- iOS全埋點解決方案-手勢採集iOS
- iOS全埋點解決方案-介面預覽事件iOS事件
- lima 輕量級虛擬機器docker替代方案 (macos平臺)虛擬機DockerMac
- 非侵入式無許可權應用內懸浮窗的實現
- 無埋點SDK實現方案(一)— 網路篇(NSURLSession)Session
- 小程式從手動埋點到自動埋點
- 埋點表相關
- 一個輕量級的iOS皮膚切換方案(內附Demo)iOS
- 輕量級分散式鎖的設計原理分析與實現分散式