iOS資料埋點統計方案選型(附Demo):執行時Method Swizzling機制與AOP程式設計(面向切面程式設計)

陳滿iOS發表於2018-04-27

傳送門:AopTestDemo

1. 場景需求

  • 統計UIViewController載入次數
  • 統計UIButton點選次數
  • 統計自定義方法的執行
  • 統計UITableView的Cell點選事件

工程說明,首頁Test1ViewController,其中有4個按鈕,點選第一個按鈕列印,第二個到第四個按鈕分別跳轉到Test2ViewController,Test3ViewController,Test4ViewController。

技術選型:

  • 手動複製統計的程式碼邏輯一個個地貼上到需要統計的類和方法中去。工作量大,可維護性差,僅適用統計埋點極少的情況。
  • 通過繼承和重寫系統方法 -- 利用寫好統計的一個基類,讓需要VC繼承自該基類,或者呼叫重寫過統計邏輯的按鈕基類等等。
  • 簡單的分類,新增類方法或者示例方法 -- 將統計邏輯封裝在分類方法裡面,在需要統計的地方匯入並呼叫分類方法。
  • 替換系統方法的分類:通過執行時Runtime的辦法 -- 利用Method Swizzling機制進行方法替換:替換原來的需要在裡面統計卻不含統計邏輯的方法 為 新的包含了統計邏輯的方法。
  • 通過AOP的方法 -- 利用Aspect框架對需要進行統計的方法進行掛鉤(hook),並注入包含了統計邏輯的程式碼塊(block)。

2. 為VC設計的分類:執行時Method Swizzling方案

iOS資料埋點統計方案選型(附Demo):執行時Method Swizzling機制與AOP程式設計(面向切面程式設計)

場景需求:需要監聽全域性的某一類的同一方法

這種方案被監聽的方法單一,但會影響全域性的所有的類的該方法。例如下面的分類,即使你不import,只要存在於工程就會影響。

  • UIViewController+Trace
#import "UIViewController+Trace.h"
#import "TraceHandler.h"
#import <objc/runtime.h>
#import <objc/objc.h>
#import "Aspects.h"


@implementation UIViewController (Trace)

#pragma mark - 1.自定義實現方法
+ (void)load{
    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}

- (void)swizzled_viewDidAppear:(BOOL)animated{
    // call original implementation
    [self swizzled_viewDidAppear:animated];
    // Begin statistics Event
    [TraceHandler statisticsWithEventName:@"UIViewController"];
}

void swizzleMethod(Class class,SEL originalSelector,SEL swizzledSelector){
    // the method might not exist in the class, but in its superclass
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    // class_addMethod will fail if original method already exists
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

    // the method doesn’t exist and we just added one
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end
複製程式碼
  • TraceHandler.m
#import "TraceHandler.h"

@implementation TraceHandler

+ (void)statisticsWithEventName:(NSString *)eventName{
    NSLog(@"-----> %@",eventName);
}

@end
複製程式碼

3. 為VC設計的分類:AOP程式設計方案

iOS資料埋點統計方案選型(附Demo):執行時Method Swizzling機制與AOP程式設計(面向切面程式設計)

場景需求:該方案的適用特點同上第二節。

Aspects 是iOS平臺一個輕量級的面向切面程式設計(AOP)框架,只包括兩個方法:一個類方法,一個例項方法。

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
複製程式碼
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
複製程式碼

函式使用方式簡單易懂,掛鉤的方式為三種:

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// 在原始方法後呼叫(預設)
    AspectPositionInstead = 1,            /// 替換原始方法
    AspectPositionBefore  = 2,            /// 在原始方法前呼叫
    
    AspectOptionAutomaticRemoval = 1 << 3 /// 在執行1次後自動移除
};
複製程式碼

呼叫示例程式碼:

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
複製程式碼

這段程式碼是給UIViewController的viewWillAppear:掛鉤一個Block,在原始方法執行完成後,列印字串。

  • UIViewController+Trace
#pragma mark - 2.使用Aspects框架
+ (void)load{
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo>aspectInfo){
                                   NSString *className = NSStringFromClass([[aspectInfo instance] class]);;
                                   [TraceHandler statisticsWithEventName:className];
                               } error:nil];
}
複製程式碼

4. 為全域性AppDelegate設計的分類:AOP程式設計方案

場景需求:需要監聽不同類,不同按鈕,系統方法,及表單元點選事件

方案特點:是可程式碼配置需要監聽的清單字典,並且需要注入的統計程式碼塊block也可以寫在這個清單裡面。

  • AppDelegate+Trace.m
#import "AppDelegate+Trace.h"
#import "TraceManager.h"

@implementation AppDelegate (Trace)

+ (void)setupLogging{
    NSDictionary *configDic = @{
                                @"ViewController":@{
                                        @"des":@"show ViewController",
                                        },
                                @"Test1ViewController":@{
                                        @"des":@"show Test1ViewController",
                                        @"TrackEvents":@[@{
                                                             @"EventDes":@"click action1",
                                                             @"EventSelectorName":@"action1",
                                                             @"block":^(id<AspectInfo>aspectInfo){
                                                                 NSLog(@"統計 Test1ViewController action1 點選事件");
                                                             },
                                                             },
                                                         @{
                                                             @"EventDes":@"click action2",
                                                             @"EventSelectorName":@"action2",
                                                             @"block":^(id<AspectInfo>aspectInfo){
                                                                 NSLog(@"統計 Test1ViewController action2 點選事件");
                                                             },
                                                             }],
                                        },
                                @"Test2ViewController":@{
                                        @"des":@"show Test2ViewController",
                                        }
                                };
    
    [TraceManager setUpWithConfig:configDic];
}

@end
複製程式碼
  • TraceManager.m
#import "TraceManager.h"

@import UIKit;

typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);

@implementation TraceManager

+ (void)setUpWithConfig:(NSDictionary *)configDic{
    // hook 所有頁面的viewDidAppear事件
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo){
                                   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                       NSString *className = NSStringFromClass([[aspectInfo instance] class]);
                                       NSString *des = configDic[className][@"des"];
                                       if (des) {
                                           NSLog(@"%@",des);
                                       }
                                   });
                               } error:NULL];
    
    for (NSString *className in configDic) {
        Class clazz = NSClassFromString(className);
        NSDictionary *config = configDic[className];
        
        if (config[@"TrackEvents"]) {
            for (NSDictionary *event in config[@"TrackEvents"]) {
                SEL selekor = NSSelectorFromString(event[@"EventSelectorName"]);
                AspectHandlerBlock block = event[@"block"];
                
                [clazz aspect_hookSelector:selekor
                               withOptions:AspectPositionAfter
                                usingBlock:^(id<AspectInfo> aspectInfo){
                                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                        block(aspectInfo);
                                    });
                                }error:NULL];
            }
        }
    }
}

@end
複製程式碼

5. 在AppDelegate的類方法中根據Plist監聽清單進行HOOK

場景需求:需要監聽不同類,不同按鈕,系統方法,及表單元點選事件

方案特點:是可程式碼配置需要監聽的清單Plist,但是不能將需要注入的統計程式碼塊block寫在這個清單Plist裡面。

  • EventList.plist

iOS資料埋點統計方案選型(附Demo):執行時Method Swizzling機制與AOP程式設計(面向切面程式設計)

  • AspectMananer.m
#pragma mark --- 監控button的點選事件
+ (void)trackBttonEvent{
    
    __weak typeof(self) ws = self;
    
    //設定事件統計
    //放到非同步執行緒去執行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //讀取配置檔案,獲取需要統計的事件列表
        NSString *path = [[NSBundle mainBundle] pathForResource:@"EventList" ofType:@"plist"];
        NSDictionary *eventStatisticsDict = [[NSDictionary alloc] initWithContentsOfFile:path];
        for (NSString *classNameString in eventStatisticsDict.allKeys) {
            //使用執行時建立類物件
            const char * className = [classNameString UTF8String];
            //從一個字串返回一個類
            Class newClass = objc_getClass(className);
            
            NSArray *pageEventList = [eventStatisticsDict objectForKey:classNameString];
            for (NSDictionary *eventDict in pageEventList) {
                //事件方法名稱
                NSString *eventMethodName = eventDict[@"MethodName"];
                SEL seletor = NSSelectorFromString(eventMethodName);
                NSString *eventId = eventDict[@"EventId"];
                
                [ws trackEventWithClass:newClass selector:seletor eventID:eventId];
                [ws trackTableViewEventWithClass:newClass selector:seletor eventID:eventId];
                [ws trackParameterEventWithClass:newClass selector:seletor eventID:eventId];
            }
        }
    });
}



#pragma mark -- 1.監控button和tap點選事件(不帶引數)
+ (void)trackEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{
    
    [klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        
        NSString *className = NSStringFromClass([aspectInfo.instance class]);
        NSLog(@"className--->%@",className);
        NSLog(@"event----->%@",eventID);
        if ([eventID isEqualToString:@"xxx"]) {
//            [EJServiceUserInfo isLogin]?[MobClick event:eventID]:[MobClick event:@"???"];
        }else{
//            [MobClick event:eventID];
        }
    } error:NULL];
}


#pragma mark -- 2.監控button和tap點選事件(帶引數)
+ (void)trackParameterEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{
    
    [klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo,UIButton *button) {
        
        NSLog(@"button---->%@",button);
        NSString *className = NSStringFromClass([aspectInfo.instance class]);
        NSLog(@"className--->%@",className);
        NSLog(@"event----->%@",eventID);
        
    } error:NULL];
}


#pragma mark -- 3.監控tableView的點選事件
+ (void)trackTableViewEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{
    
    [klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo,NSSet *touches, UIEvent *event) {
        
        NSString *className = NSStringFromClass([aspectInfo.instance class]);
        NSLog(@"className--->%@",className);
        NSLog(@"event----->%@",eventID);
        NSLog(@"section---->%@",[event valueForKeyPath:@"section"]);
        NSLog(@"row---->%@",[event valueForKeyPath:@"row"]);
        NSInteger section = [[event valueForKeyPath:@"section"]integerValue];
        NSInteger row = [[event valueForKeyPath:@"row"]integerValue];
        
        //統計事件
        if (section == 0 && row == 1) {
//            [MobClick event:eventID];
        }
        
    } error:NULL];
}
複製程式碼
  • Appdelegate.m呼叫
[AspectMananer trackBttonEvent];
複製程式碼

相關文章