無侵入埋點

什麼是埋點?

埋點是一種瞭解用戶行爲,分析用戶行爲,提升用戶體驗的一種方式。 常見的解決方案有三種,代碼埋點、可視化埋點、和無埋點三種。bash

  • 代碼埋點主要就是經過手寫代碼的方式來埋點,能很精確的在須要埋點的地方,添加代碼。存在開發量大,後期難以維護的問題。
  • 可視化埋點,將埋點的增長和修改可視化,提高了增長和維護埋點的體驗。
  • 無埋點又叫全埋點,埋點代碼不會出如今業務代碼中,容易管理和維護,缺點是成本高,解析複雜。

無埋點能夠作到,埋點被統一維護,與業務代碼解耦,知足大部分需求。markdown

無埋點的實現

無埋點的實現主要是基於,runtime,在運行時,替換原有方法,實現埋點。app

創建替換方法的類

新建一個class,主要代碼以下,ide

#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);
    if (class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(fromMethod))) {
        class_replaceMethod(class, toselector, method_getImplementation(fromMethod), method_getTypeEncoding(toMethod));
    }else{
        method_exchangeImplementations(fromMethod, toMethod);
    }
}
@end
複製代碼

其中主要的做用是交換兩個IMP指針的實現spa

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
複製代碼

IMP是具體方法的實現。指針

頁面的進入次數和停留時間

分析:獲取頁面的停留時間,主要hook住UIViewControllerviewWillAppear:viewWillDisappear:的兩個方法,獲取執行這兩個方法的時間差,就能夠獲取停留時長 ,給UIViewController建一個Category 實現方法code

#import "ViewController+time.h"
#import "SMHook.h"
@implementation UIViewController (logger)

+(void)initialize{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL fromSelector = @selector(viewWillAppear:);
        SEL toSelector = @selector(hook_viewWillAppear:);
        [SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
        
        SEL fromDisappear = @selector(viewWillDisappear:);
        SEL toDisappear = @selector(hook_viewWillDisAppear:);
        [SMHook hookClass:self fromSelector:fromDisappear toSelector:toDisappear];
    });
}
複製代碼

hook方法的實現orm

-(void)hook_viewWillAppear:(BOOL)animated{
    NSLog(@"hook viwe");
    // 進來的時間 根據具體的業務去加時間的統計
    [self comeIn];
    [self hook_viewWillAppear:animated];
}

-(void)hook_viewWillDisAppear:(BOOL)animated{
    // 出去的時間 統計方法根據具體的業務加
    [self comeOut];
    [self hook_viewWillDisAppear:animated];
}

複製代碼

注意: 在實現了 hook_viewWillAppear:方法後,又調用了一遍 [self hook_viewWillAppear:animated];這裏的IMP執行的是viewWillAppear:方法,注意前面的對象

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
複製代碼

方法,這樣不會由於hook住viewWillAppear:的實現,而影響了業務代碼中,viewWillAppear:內容的實現,並不會形成循環調用。事件

點擊事件的HOOK

UIButton的點擊事件的hook和UIViewControllerviewWillAppear:的hook基本上同樣, 這裏面要hook的是:

// send the action. the first method is called for the event and is a point at which you can observe or override behavior. it is called repeately by the second.

- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
複製代碼

這個方法,在點擊的時候會被調用。 不是

- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
複製代碼

這個方法。

給UIButton新建Category,主要有三步

  1. 替換方法
  2. hook住方法的實現
  3. 上傳埋點信息
#import "UIButton+logger.h"
#import "SMHook.h"
#import <objc/runtime.h>
@implementation UIButton (logger)
+(void)initialize{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        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 insertAction:action to:target forEvent:event];
    [self hook_sendAction:action to:target forEvent:event];
    
}
-(void)insertAction:(SEL)action to:(id)target forEvent:(UIEvent*)event{
    NSString * actionName = NSStringFromSelector(action);
    NSString * targetName = NSStringFromClass([target class]);
    NSString *name = self.name;
    UIView *targetView = (UIView *)target;
    NSLog(@"%@",targetView);
    NSLog(@"button name == %@ actionName == %@, targetName == %@",name, actionName,targetName);
    // 缺乏獲取view_path的方法
    NSLog(@"viewPath %@",viewPath);
}

複製代碼

這裏有個關鍵,不單單是UIButton,而是任何你想Hook住對象的View_Path

爲何要View_path

每一個控件須要有惟一的標識,這樣才能對其埋點進行分析,若是一個視圖下有多個UIButton,這樣就不能僅僅經過 actionName 和 targetName 對齊分析 有一種解決辦法是經過視圖的層級結構,樹狀結構來分析 以下:

ViewController[0]/UIView[0]/UITableView[0]/UITableViewCell[0:2]/UIButton[0]
複製代碼

其中:

  • 經過此標識能夠在當前頁面 view 樹形結構中惟一的肯定此元素。
  • 標識的每一項由兩部分組成:一是當前元素的 class 的字符串表示,二是當前元素在同級元素中的序號,自 0 開始計算。如當前第二個 UIImageView,則是 UIImageView1。
  • 標識不一樣項之間以 / 拼接。
  • 標識的最頂層是當前 view 所在的 ViewController。
  • 對於 UITableViewCellUICollectionViewCell 及相似的自定義組件,序號部分由兩部分組成:sectionrow,並以: 拼接。
  • 標識的最末端是當前被點擊或觸摸的元素

具體的實現:

+(NSString *)viewPath:(UIView *)currentView{
    __block NSString *viewPath = @"";
    
    for (UIView *view = currentView;view;view = view.superview) {
        NSLog(@"%@",view);
        if ([view isKindOfClass:[UICollectionViewCell class]]) {
            // 是一個
            UICollectionViewCell *cell = (UICollectionViewCell *)view;
            UICollectionView *cv = (UICollectionView *)cell.superview;
            NSIndexPath *indexPath = [cv indexPathForCell:cell];
            NSString *className = NSStringFromClass([cell class]);
            viewPath = [NSString stringWithFormat:@"%@[%ld:%ld]/%@",className,indexPath.section,indexPath.row,viewPath];
            continue;
        }
        
        if ([view isKindOfClass:[UITableViewCell class]]) {
            // 是一個
            UITableViewCell *cell = (UITableViewCell *)view;
            UITableView *tb = (UITableView *)cell.superview;
            NSIndexPath *indexPath = [tb indexPathForCell:cell];
            NSString *className = NSStringFromClass([cell class]);
            viewPath = [NSString stringWithFormat:@"%@[%ld:%ld]/%@",className,indexPath.section,indexPath.row,viewPath];
            continue;
        }
        
        
        if ([view isKindOfClass:[UIView class]]) {
            [view.superview.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                if (obj == view) {
                    NSString *className = NSStringFromClass([view class]);
                    viewPath = [NSString stringWithFormat:@"%@[%ld]/%@",className,idx,viewPath];
                    *stop = YES;
                }
            }];
        }
        
        UIResponder *responder = [view nextResponder];
        if ([responder isKindOfClass:[UIViewController class]]) {
            
            NSString *className = NSStringFromClass([responder class]);
            viewPath = [NSString stringWithFormat:@"%@/%@",className,viewPath];
            return viewPath;
        }
    }
    return viewPath;
}
複製代碼

感受不是最優解,哈哈, 以上能夠拿到。一個元素在當前控制器的路徑。能夠以此進行數據分析。 以上結束。謝謝!

相關文章
相關標籤/搜索