iOS:hook實(shí)現(xiàn)無侵入式埋點(diǎn)、統(tǒng)計(jì)

今天來說說iOS的埋點(diǎn)。

不管是埋點(diǎn),統(tǒng)計(jì)還是什么其他辟邪劍譜,主要的目的是為了了解用戶行為習(xí)慣,進(jìn)而開發(fā)出更友好的APP。
埋點(diǎn)的形式主要有:

1.統(tǒng)計(jì)頁(yè)面停留時(shí)長(zhǎng)
2.頁(yè)面出現(xiàn)次數(shù)
3.按鈕的點(diǎn)擊次數(shù)

在技術(shù)上,埋點(diǎn)主要包括代碼埋點(diǎn)、可視化埋點(diǎn)和全埋點(diǎn)。

埋點(diǎn)方式 優(yōu)點(diǎn) 缺點(diǎn)
代碼埋點(diǎn)(侵入式) 方便靈活,什么樣的埋點(diǎn)都可以實(shí)現(xiàn)。包括各種奇葩埋點(diǎn)。 維護(hù)成本高,由于到處都是埋點(diǎn)的代碼,所以清理維護(hù)難
可視化埋點(diǎn)/全埋點(diǎn)(非侵入式) 埋點(diǎn)統(tǒng)一維護(hù),解耦。適用于大量通用的埋點(diǎn) 不適用所有,唯一標(biāo)識(shí)難以確定,開發(fā)成本較大

侵入式埋點(diǎn)確實(shí)沒什么可說的,主要是將埋點(diǎn)統(tǒng)計(jì)代碼寫在需要埋點(diǎn)的\color{rgb(255,0,0)}{view、viewControll} 的具體類里面,所以主要說說非侵入式埋點(diǎn)。

非侵入式埋點(diǎn)

利用\color{rgb(255,0,0)}{runtime},交換系統(tǒng)方法,并實(shí)現(xiàn)埋點(diǎn)邏輯

#import <objc/runtime.h>
@interface OBAspect : NSObject
+ (void)ob_hookTarget:(id)target originSelector:(SEL)osel newSelector:(SEL)nsel;
@end

@implementation OBAspect
//未做非空判斷,實(shí)際還需要驗(yàn)證方法的實(shí)現(xiàn)是否完整
+ (void)ob_hookTarget:(id)target originSelector:(SEL)osel newSelector:(SEL)nsel {
    Method m1 = class_getInstanceMethod(target, osel);
    Method m2 = class_getInstanceMethod(target, nsel);
    method_exchangeImplementations(m1, m2);
}
@end

按鈕這里最主要是,找到這個(gè)點(diǎn)擊事件的方法\color{rgb(255,0,0)}{sendAction:to:forEvent:} ,然后在 \color(rgb(255,0,0)}{+load()} 方法使用 $\color{rgb(255,0,0)}{OBAspect hook}方法

@interface UIButton(OBCount)

@end

@implementation UIButton(OBCount)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [OBAspect ob_hookTarget:self originSelector:@selector(sendAction:to:forEvent:) newSelector:@selector(ob_sendAction:to:forEvent:)];
    });
}

- (void)ob_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    NSLog(@"點(diǎn)擊了[%@-%@-%@]",NSStringFromClass([target class]),NSStringFromSelector(action),self.titleLabel.text);
    [self ob_sendAction:action to:target forEvent:event];
}
@end

頁(yè)面統(tǒng)計(jì)時(shí):頁(yè)面進(jìn)入次數(shù)、頁(yè)面停留時(shí)間都是對(duì) \color{rgb(255,0,0)}{UIViewController} 生命周期函數(shù)進(jìn)行\color{rgb(255,0,0)}{hook},然后埋點(diǎn)。創(chuàng)建一個(gè) \color{rgb(255,0,0)}{UIViewController}\color{rgb(255,0,0)}{Category}

@interface UIViewController(OBCount)
@end

@implementation UIViewController(OBCount)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [OBAspect ob_hookTarget:self originSelector:@selector(viewWillAppear:) newSelector:@selector(ob_viewWillAppear:)];
         [OBAspect ob_hookTarget:self originSelector:@selector(viewWillDisappear:) newSelector:@selector(ob_viewWillDisappear:)];
    });
}

- (void)ob_viewWillAppear:(BOOL)animated {
    NSLog(@"進(jìn)入了%@",NSStringFromClass([self class]));
    [self ob_viewWillAppear:animated];
}
- (void)ob_viewWillDisappear:(BOOL)animated {
    NSLog(@"退出了%@",NSStringFromClass([self class]));
    [self ob_viewWillDisappear:animated];
}

然后運(yùn)行

Test_Hook[11462:12348762] 進(jìn)入了ViewController
Test_Hook[11462:12348762] 點(diǎn)擊了[ViewController-loginClick:-登錄]
Test_Hook[11462:12348762] 退出了ViewController
Test_Hook[11462:12348762] 進(jìn)入了HomeViewController

注意:要找到\color{rgb(255,0,0)}{view}的唯一性,可以是多個(gè)信息組合,比如\color{rgb(255,0,0)}{NSStringFromClass([target class]),NSStringFromSelector(action)}組合,再不行加上\color{rgb(255,0,0)}{text}

如何對(duì)cell埋點(diǎn)?

推薦:無埋點(diǎn)和可視化埋點(diǎn)
UITableView的cell有緩存機(jī)制,每個(gè)cell的點(diǎn)擊事件的埋點(diǎn)的關(guān)鍵是唯一標(biāo)識(shí)符的制定規(guī)則;

唯一標(biāo)識(shí)符的制定規(guī)則

利用\color{rgb(255,0,0)}{-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath}方法中下標(biāo),可以確定是點(diǎn)擊的哪個(gè)cell,我們?cè)谥贫?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Ccolor%7Brgb(255%2C0%2C0)%7D%7B%E5%94%AF%E4%B8%80%E6%A0%87%E8%AF%86%E7%AC%A6%7D" alt="\color{rgb(255,0,0)}{唯一標(biāo)識(shí)符}" mathimg="1">的時(shí)候,可以是\color{rgb(255,0,0)}{HomeVC_1_2}這樣的格式,表示在首頁(yè)的tableView中,第1個(gè)section的第2個(gè)cell,這樣就可以確定\color{rgb(255,0,0)}{唯一標(biāo)識(shí)符},在根據(jù)這個(gè)\color{rgb(255,0,0)}{唯一標(biāo)識(shí)符}可以去NSDictionary中查找對(duì)應(yīng)的value,保存并上傳數(shù)據(jù)。

  NSDictionary *dict = @{
                           @"HomeVC_1_0":@"男士",
                           @"HomeVC_1_1":@"女士",
                           @"HomeVC_1_2":@"兒童"
                           };
// 啟動(dòng)優(yōu)化,load方法的邏輯延后執(zhí)行
+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //交換tableView的代理方法
        Method m1 = class_getInstanceMethod([self class], @selector(setDelegate:));
        Method m2 = class_getInstanceMethod([self class], @selector(setOBDelegate:));
        method_exchangeImplementations(m1, m2);
    });
}

//此處可以 獲取 tableView的代理
- (void)setOBDelegate:(id<UITableViewDelegate>)delegate {
    Class cla = [delegate class];
    //1:拿到了代理,交換代理的點(diǎn)擊方法
    SEL originalSelector = @selector(tableView:didSelectRowAtIndexPath:);
    SEL swizzledSelector = @selector(ob_tableView:didSelectRowAtIndexPath:);
    
    Method originalMethod = class_getInstanceMethod(cla, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(cla, swizzledSelector);
  
    BOOL add = NO;
    //2:原方法可以肯定有,但是替換的方法不一定有。所以沒有就要add
    if (swizzledMethod == nil) {
        //方法的實(shí)現(xiàn)是在self中,不能寫在delegate中,否則耦合
        IMP imp = class_getMethodImplementation([self class], swizzledSelector);
        const char* types = method_getTypeEncoding(swizzledMethod);
        add = class_addMethod(cla, swizzledSelector, imp, types);
        
        swizzledMethod = class_getInstanceMethod(cla, swizzledSelector);
    }
    //3:兩個(gè)方法都有了,交換方法實(shí)現(xiàn)
     method_exchangeImplementations(originalMethod, swizzledMethod);
    
    //設(shè)置原來的代理
    [self setOBDelegate:delegate];
}

- (void)ob_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    //code 埋點(diǎn)的代碼邏輯
    
    [lock lock]; //寫入文件時(shí)注意加鎖
    //
    NSString * str = [NSString stringWithFormat:@"%ld-%ld",indexPath.section,indexPath.row];
    
   [lock unlock];
    
    
    [self ob_tableView:tableView didSelectRowAtIndexPath:indexPath];
}

cell的出現(xiàn)時(shí)長(zhǎng)統(tǒng)計(jì)?

思路:在滑動(dòng)停止時(shí),對(duì)出現(xiàn)在屏幕上的cell做計(jì)時(shí)或者設(shè)置開始時(shí)間,然后頁(yè)面消失或者滑動(dòng)tableView時(shí),再次停止時(shí),對(duì)比前后的出現(xiàn)在屏幕上的cell,如果cell還在屏幕上,繼續(xù)計(jì)時(shí),如果cell滑出屏幕了,計(jì)時(shí)停止,并做好統(tǒng)計(jì),更新,以便下次出現(xiàn)使用
可以hook UITableView 的\color{rgb(255,0,0)}{tableView:willDisplayCell:forRowAtIndexPath:}或者UICollectionView的\color{rgb(255,0,0)}{collectionView:willDisplayCell:forRowAtIndexPath:}方法

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"cell------------");
}

- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath {

}

注意點(diǎn):這個(gè)兩個(gè)方法是實(shí)時(shí)調(diào)用的,考慮到性能,要等到頁(yè)面滑動(dòng)停止了,再開始計(jì)算,(如果滑動(dòng)過程中就開始計(jì)算,那么是不正確的,因?yàn)榛瑒?dòng)時(shí),用戶看不清楚)所以,計(jì)算要等到結(jié)束時(shí)計(jì)時(shí),可以通過\color{rgb(255,0,0)}{[tableView performSelector:@selector(hlj_calculateViewVisible:) withObject:dict afterDelay:0 inModes:@[NSDefaultRunLoopMode]];},只有在切換到\color{rgb(255,0,0)}{NSDefaultRunLoopMode}才會(huì)計(jì)算

或者h(yuǎn)ook這兩個(gè)函數(shù):\color{rgb(255,0,0)}{tableView.visibleCells}\color{rgb(255,0,0)}{tableView.indexPathsForVisibleRows},也是可以得到當(dāng)前屏幕中的cell,然后取差集。

\color{rgb(255,0,0)}{VisibleCells:}出現(xiàn)在屏幕上的cell,沒有下標(biāo)
\color{rgb(255,0,0)}{indexPathsForVisibleRows:}出現(xiàn)在屏幕上的cell,有下標(biāo),可以確定唯一標(biāo)識(shí)符

如何取差集

利用一個(gè)記錄上一次的cell的\color{rgb(255,0,0)}{lastDictionary}字典,第二次頁(yè)面停止時(shí)會(huì)產(chǎn)生一個(gè)currentDictionary,遍歷\color{rgb(255,0,0)}{currentDictionary},把里面的key取出來,然后去\color{rgb(255,0,0)}{lastDictionary}里面刪除這個(gè)key(如果有這個(gè)key),那么最后剩下來的\color{rgb(255,0,0)}{lastDictionary}就是第一次出現(xiàn)在屏幕中,第二次消失的cell,對(duì)他計(jì)算統(tǒng)計(jì),然后在需要將\color{rgb(255,0,0)}{lastDictionary}重新賦值。

埋點(diǎn)數(shù)據(jù)如何上傳?

埋點(diǎn)分兩種:一種是普通埋點(diǎn),另一種是日志埋點(diǎn);

1:普通埋點(diǎn)

滿足三個(gè)條件上傳:1:進(jìn)入APP時(shí);2:進(jìn)入后臺(tái)時(shí);3:使用時(shí)

1:進(jìn)入APP時(shí)

\color{rgb(255,0,0)}{-application: didFinishLaunchingWithOptions:}或者進(jìn)入前臺(tái)時(shí),檢測(cè)本地有沒有埋點(diǎn)數(shù)據(jù),有就上傳。沒有就不上傳

如果系統(tǒng)crash,或者被kill時(shí),需要將緩存中的數(shù)據(jù)保存到本地,下次打開上傳。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //去除埋點(diǎn)數(shù)據(jù),有就上傳,沒有作罷
    NSDictionary *dict = [[MDManager shared] readDataFormManager];
    //注冊(cè)crash時(shí)的回調(diào),crash時(shí),可以執(zhí)行保存的操作
    NSSetUncaughtExceptionHandler(exceptionHandler);
    return YES;
    
}
// 系統(tǒng)crash時(shí)執(zhí)行
void exceptionHandler(NSException *exception) {
    //保存數(shù)據(jù)
    [[MDManager shared] saveToDisk];
}
// 系統(tǒng)被回收時(shí)執(zhí)行
- (void)applicationWillTerminate:(UIApplication *)application {
   //保存數(shù)據(jù)
    [[MDManager shared] saveToDisk];
}

2:進(jìn)入后臺(tái)時(shí)

進(jìn)入后臺(tái)時(shí):有就上傳。沒有就不上傳

3:使用時(shí)

如果設(shè)置數(shù)據(jù)上限,比如當(dāng)數(shù)據(jù)大于 100k時(shí),立即上傳

注意:上傳時(shí),也有埋點(diǎn)數(shù)據(jù),需要小心處理,不然會(huì)數(shù)據(jù)丟失

這里不加讀寫鎖(或者是CGD的柵欄函數(shù)),目的是首先不加鎖也能實(shí)現(xiàn)數(shù)據(jù)不丟失。其次加鎖會(huì)對(duì)每次寫入有額外的性能消耗。

- (void)uploadDataFromMemory {
    NSDictionary * updict = [self.dict copy];
    dispatch_queue_t q = dispatch_queue_create("ob", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(q, ^{
        
        // code .. 上傳代碼
        //上傳成功后刪,除上傳的部分
        [updict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            [self.dict removeObjectForKey:key];
        }];
        
    });
}

部分cell埋點(diǎn)代碼

#import "UITableView+MDTableView.h"
#import <objc/runtime.h>
#import "MDManager.h"

@implementation UITableView (MDTableView)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method m1 = class_getInstanceMethod([self class], @selector(setDelegate:));
        Method m2 = class_getInstanceMethod([self class], @selector(setMDDelegate:));
        method_exchangeImplementations(m1, m2);
    });
}
+ (void)initialize {
    
}

- (void)setMDDelegate:(id<UITableViewDelegate>)delegate {
    
    SEL s1 = @selector(tableView:willDisplayCell:forRowAtIndexPath:);
    Method m1 = class_getInstanceMethod([delegate class], s1);
    if (m1 == nil) {
        SEL s1_no = @selector(MD_No_tableView:willDisplayCell:forRowAtIndexPath:);
        Method m1_no = class_getInstanceMethod([self class], s1_no);
        IMP imp1 = method_getImplementation(m1_no);
        const char * types_s1 = method_getTypeEncoding(m1_no);
        BOOL add_m1 = class_addMethod([delegate class], s1, imp1, types_s1);
        if (add_m1) {
            //
            m1 = class_getInstanceMethod([delegate class], s1);
        } else {
            //
        }
    }
    
    SEL s2 = @selector(MD_tableView:willDisplayCell:forRowAtIndexPath:);
    Method m2 = class_getInstanceMethod([self class], s2);
    IMP imp2 = method_getImplementation(m2);
    const char * types = method_getTypeEncoding(m2);
    BOOL add = class_addMethod([delegate class], s2, imp2, types);
//
    if (add) {
        Method m22 = class_getInstanceMethod([delegate class], s2);
        method_exchangeImplementations(m1, m22);
    }
    
//hook
    
    [self setMDDelegate:delegate];
}

- (void)MD_tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSDictionary * dict = @{@"cell":cell,@"indexPath":indexPath,@"tableView":tableView};
    [tableView performSelector:@selector(hlj_calculateViewVisible:) withObject:dict afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
    [self MD_tableView:tableView willDisplayCell:cell forRowAtIndexPath:indexPath];
}

- (void)hlj_calculateViewVisible:(NSDictionary *)dict {
    UIView * view = dict[@"cell"];
    NSIndexPath *ip = dict[@"indexPath"];
    UITableView *tb = dict[@"tableView"];
    NSString *str = [NSString stringWithFormat:@"%@_%@_%@_",[tb.delegate class],[tb class],[view class]];
    [[MDManager shared] viewExposure:[NSString stringWithFormat:@"%@%ld-%ld",str,(long)ip.section,(long)ip.row]];
}

- (void)MD_No_tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"========");
}

@end

參考鏈接:https://blog.csdn.net/pk_sir/article/details/107177963

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容