前言 我們先瞭解 UIViewController 生命周期相關的內容和 iOS 的“黑魔法” Method Swizzling。然後再瞭解頁面瀏覽事件($AppViewScreen)全埋點的實現原理 一、UIViewController 生命周期 眾所周知,每一個 UIViewContro ...
前言
我們先瞭解 UIViewController 生命周期相關的內容和 iOS 的“黑魔法” Method Swizzling。然後再瞭解頁面瀏覽事件($AppViewScreen)全埋點的實現原理
一、UIViewController 生命周期
眾所周知,每一個 UIViewController 都管理著一個由多個視圖組成的樹形結構,其中根視圖保存在 UIViewController 的 view 屬性中。UIViewController 會懶載入它所管理的視圖集,直到第一次訪問 view 屬性時,才會去載入或者創建 UIViewController 的視圖集。
有以下幾種常用的方式載入或者創建 UIViewController 的視圖集:
- 使用 Storyboard
- 使用 Nib 文件
- 使用代碼,即重寫 - loadView
以上這些方法,最終都會創建出合適的根視圖並保存在 UIViewController 的 view 屬性中,這是 UIViewController 生命周期的第一步。當 UIViewController 的根視圖需要展示在頁面上時,會調用 - viewDidLoad 方法。在這個方法中,我們可以做一些對象初始化相關的工作。
需要註意的是:此時,視圖的 bounds 還沒有確定。對於使用代碼創建視圖,- viewDidLoad 方法會在 -loadView 方法調用結束之後運行;如果使用的是 Stroyboard 或者 Nib 文件創建視圖,- viewDidLoad 方法則會在 - awakeFromNib 方法之後調用。
當 UIViewController 的視圖在屏幕上的顯示狀態發生變化時,UIViewController 會自動回調一些方法,確保子類能夠響應到這些變化。如下圖所示,它展示了 UIViewController 在不同的顯示狀態時會回調不同的方法。
在 UIViewController 被銷毀之前,還會回調 - dealloc 方法,我們一般通過重寫這個方法來主動釋放不能被 ARC 自動釋放的資源。
我們現在對 UIViewController 的整個生命周期有了一些基本瞭解。那麼,我們如何去實現頁面瀏覽事件( $AppViewScreen 事件)的全埋點呢?
通過 UIViewController 的生命周期可知,當執行到 - viewDidAppear: 方法時,表示視圖已經在屏幕上渲染完成,也即頁面已經顯示出來了,正等待用戶進行下一步操作。因此,- viewDidAppear: 方法就是我們觸發頁面瀏覽事件的最佳時機。如果想要實現頁面瀏覽事件的全埋點,需要使用 iOS 的“黑魔法” Method Swizzling 相關的技術。
二、Method Swizzling 黑魔法
Method Swizzling,顧名思義,就是交換兩個方法的實現。簡單的來說,就是利用 Objective-C runtime 的動態綁定特性,把一個方法的實現與另一個方法的實現進行交換。
2.1 Method Swizzling 基礎
在 Objective-C 的 runtime 中,一個類是用一個名為 objc_class 的結構體表示的,它的定義如下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在上面的結構體中,雖然有很多欄位在 OBJC2 中已經廢棄了(OBJC2_UNAVAILABLE),但是瞭解這個結構體還是有助於我們理解 Method Swizzling 的底層原理。我們從上述結構體中可以發現,有一個 objc_method_list 指針,它保存著當前類的所有方法列表。同時,objc_method_list 也是一個結構體,它的定義如下:
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
在上面的結構體中,有一個 objc_method 欄位,我們再來看看 objc_method 這個結構體:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
從上面的結構體中可以看出,一個方法由下麵三個部分組成:
- method_name:方法名
- method_types:方法類型
- method_imp:方法實現
使用 Method Swizzling 交換方法,其實就是修改了 objc_method 結構體中的 method_imp,也即改變了 method_name 和 method_imp 的映射關係,如下圖所示。
那我們如何改變 method_name 和 method_imp 的映射關係呢?在 Objective-C 的 runtime 中,提供了很多非常方便使用的函數,讓我們可以很簡單的就能實現 Method Swizzling,即改變 method_name 和 method_imp 的映射關係,從而達到交換方法的效果。
2.2 實現 Method Swizzling 的相關函數
-
Method class_getInstanceMethod
// 返回目標類 aClass、方法名為 aSelector 的實例方法 // aClass :目標類 // aSelector: 方法名 OBJC_EXPORT Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
-
BOOL class_addMethod
// 給目標類 aClass 添加一個新的方法,同時包括方法的實現 // aClass: 目標類 // aSelector: 要添加方法的方法名 // imp: 要添加方法的方法實現 // types: 方法實現的編碼類型 OBJC_EXPORT BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
-
IMP method_getImplementation
// 返回方法實現的指針 // 目標方法 OBJC_EXPORT IMP _Nonnull method_getImplementation(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
-
IMP class_replaceMethod
// 替換目標類 aClass 的 aSelector 方法指針 // aClass: 目標類 // aSelector: 目前方法的方法名 // imp:新方法的方法實現 // types: 方法實現的編碼類型 OBJC_EXPORT IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
-
void method_exchangeImplementations
// 交換2個方法的實現指針 // m1: 交換方法1 // m2: 交換方法2 OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
2.3 實現 Method Swizzling
第一步 創建 NSObject 的分類 NSObject+SASwizzler
第二步 在 NSObject+SASwizzler.h 聲明方法交換方法
/// 交換方法名為 originalSEL 和方法名為 alternateSEL 兩個方法實現
/// @param originalSEL 原始的方法名稱
/// @param alternateSEL 要交換的方法名稱
+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL;
第三步 在 NSObject+SASwizzler.m 實現方法的交換
+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL {
// 獲取原始方法
Method originalMethod = class_getInstanceMethod(self, originalSEL);
// 當原始的方法不存在時,返回NO,表示 Swizzler 失敗
if (!originalMethod) {
return NO;
}
// 獲取要交換的方法
Method alternateMethod = class_getInstanceMethod(self, alternateSEL);
// 當交換的方法不存在時,返回NO,表示 Swizzler 失敗
if (!alternateMethod) {
return NO;
}
// 交換兩個方法的實現
method_exchangeImplementations(originalMethod, alternateMethod);
return YES;
}
三、實現界面預覽事件全埋點
利用方法交換,來交換 UIViewController 的 -viewDidAppear: 方法,然後在方法交換中觸發 $AppViewScreen 事件,來實現界面預覽的全埋點。
3.1 實現步驟
第一步:在 SensorsSDK 項目中,新增一個 UIViewController 類別 UIViewController+SensorsData
第二步:在 UIViewController+SensorsData.m 類別新增交換方法 - sensorsdata_viewDidAppear:,然後再交換方法中調用原始方法,並觸發 $AppViewScreen 事件
- (void)sensorsdata_viewDidAppear:(BOOL)animated {
// 調用原始方法, 即 - viewDidAppear
[self sensorsdata_viewDidAppear:animated];
// 觸發 $AppViewScreen 事件
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
[properties setValue:NSStringFromClass([self class]) forKey:@"$screen_name"];
[properties setValue:self.navigationItem.title forKey:@"$title"];
[[SensorsAnalyticsSDK sharedInstance] track:@"$AppViewScreen" properties:properties];
}
第三步: 在 UIViewController+SensorsData.m 中重寫 + load 類方法,併在 + load 類方法中調用 NSObject+SASwizzler 的類方法交換
+ (void)load {
[UIViewController sensorsdata_swizzleMethod:@selector(viewDidAppear:) withMethod:@selector(sensorsdata_viewDidAppear:)];
}
第四步 : 測試驗證
{
"event" : "$AppViewScreen",
"time" : 1648626597682,
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$app_version" : "1.0",
"$screen_name" : "ViewController",
"$os_version" : "15.2",
"$lib" : "iOS"
}
}
3.2 優化
問題:在應用程式啟動過程中,會觸發多餘的 $AppViewScreen ,我們可以引入黑名單的機制,即在黑名單里配置那些 UIViewController 及子類不觸發 $AppViewScreen 事件。
第一步 創建一個 sensorsdata_black_list.plist 文件,並把 root 類型改成 Array,該文件就是黑名單文件,然後在黑名單文件中添加控制器,如圖所示:
第二步 在 UIViewController+SensorsData.m 文件中新增 - shouldTrackAppViewScreen 方法,用來判斷當前控制器是否在黑名單中。
static NSString * const kSensorsDataBlackListFileName = @"sensorsdata_black_list";
// 黑名單
- (BOOL)shouldTrackAppViewScreen {
static NSSet *blackList = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle bundleForClass:SensorsAnalyticsSDK.class] pathForResource:kSensorsDataBlackListFileName ofType:@"plist"];
NSArray *classNames = [NSArray arrayWithContentsOfFile:path];
NSMutableSet *set = [NSMutableSet setWithCapacity:classNames.count];
for (NSString *className in classNames) {
[set addObject:NSClassFromString(className)];
}
blackList = [set copy];
});
for (Class cla in blackList) {
if ([self isKindOfClass:cla]) {
return NO;
}
}
return YES;
}
第三步 在觸發 $AppViewScreen 事件之前,判斷是否在黑名單中
- (void)sensorsdata_viewDidAppear:(BOOL)animated {
// 調用原始方法, 即 - viewDidAppear
[self sensorsdata_viewDidAppear:animated];
// 觸發 $AppViewScreen 事件
if ([self shouldTrackAppViewScreen]) {
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
[properties setValue:NSStringFromClass([self class]) forKey:@"$screen_name"];
[properties setValue:self.navigationItem.title forKey:@"$title"];
[[SensorsAnalyticsSDK sharedInstance] track:@"$AppViewScreen" properties:properties];
}
}
第四步 測試驗證
運行Demo,所添加到黑名單中的 controller 不會發送 $AppViewScreen 事件。
3.4 遺留問題
按照目前的方案實現 $AppViewScreen 事件的全埋點,會有2個問題:
應用程式熱啟動是(從後臺恢復),第一個界面沒有觸發 $AppViewScreen 事件。原因是這個界面沒有再次執行 - viewDidAppear: 方法
要求 UIViewController 的子類不重寫 -viewDidAppear:方法,一旦重寫,必須調用[super viewDidAppear:animated], 否則不會觸發 $AppViewScreen 事件。原因是直接交換了 UIViewController 的 - viewDidAppear: 方法。