iOS全埋點解決方案-界面預覽事件

来源:https://www.cnblogs.com/r360/archive/2022/04/01/16085423.html
-Advertisement-
Play Games

前言 ​ 我們先瞭解 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 在不同的顯示狀態時會回調不同的方法。

image-20220330104814891

​ 在 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 的映射關係,如下圖所示。

image-20220330115713273

那我們如何改變 method_name 和 method_imp 的映射關係呢?在 Objective-C 的 runtime 中,提供了很多非常方便使用的函數,讓我們可以很簡單的就能實現 Method Swizzling,即改變 method_name 和 method_imp 的映射關係,從而達到交換方法的效果。

2.2 實現 Method Swizzling 的相關函數

  1. 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);
    
  2. 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);
    
  3. IMP method_getImplementation

    // 返回方法實現的指針
    // 目標方法
    OBJC_EXPORT IMP _Nonnull
    method_getImplementation(Method _Nonnull m) 
        OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
  4. 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);
    
  5. 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,該文件就是黑名單文件,然後在黑名單文件中添加控制器,如圖所示:

image-20220330164145172

第二步 在 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: 方法。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 鏡像下載、功能變數名稱解析、時間同步請點擊 阿裡雲開源鏡像站 ​LNMP是Linux + Nginx + MySQL + PHP 四個系統的首字母縮寫,相對於 LAMP(Linux + Apache + MySQL + PHP )來說的。曾經在虛擬主機建站界風靡一時,隨著新的編程語言和容器技術、微服務等發展 ...
  • 我們可以通過公共倉庫拉取鏡像使用,但是,有些時候公共倉庫拉取的鏡像並不符合我們的需求。儘管已經從繁瑣的部署工作中解放出來了,但是在實際開發時,我們可能希望鏡像包含整個項目的完整環境,在其他機器上拉取打包完整的鏡像,直接運行即可。 ​ Docker 支持自己構建鏡像,還支持將自己構建的鏡像上傳到公共倉 ...
  • 項目場景:一次線上MySQL死鎖告警原因排查 最近處理了一次線上數據告警,記錄一下。 問題描述 同步書架書籍的介面頻繁拋出異常,提示資料庫出現死鎖,異常如下: 本日異常次數:2,異常日誌:java.lang.RuntimeException: org.springframework.dao.Dead ...
  • 使用redis作為緩存時,存在一些應用問題,包括緩存穿透、緩存擊穿、緩存雪崩。 ...
  • 寫在前面 Clickhouse 從 21.11 版本開始,除了提供類似SqlServer、MySQL CREATE FUNCTION 的自定義函數之外,還有一個用戶自定義函數(UDF),與其說是“用戶自定義函數”,為了避免混淆,稱之為”用戶自定義外部函數“更為準確。官方對此功能的解釋: ClickH ...
  • 疫情期間,很多線下活動轉為線上舉行,實時音視頻的需求劇增,在視頻會議,線上教育,電商購物等眾多場景成了“生活新常態”。 本文將教你如何通過即構ZEGO sdk在Android端搭建視頻通話能力。即構SDK提供100+種行業解決方案,每月贈送10000分鐘免費時長,提供免費接入體驗。 接下來我們看... ...
  • 一、創建一個Flutter工程🔥 🔺1.1 命令行創建 首先我們找一個空目錄用來專門存放flutter項目,然後在路徑中直接輸入cmd: 使用 flutter create <projectname> 命令創建flutter項目: 創建成功: 進入項目根目錄中,執行 flutter run 命令 ...
  • 本次為大家帶來的是 DevEco Device Tool 3.0 Release,新增四項新功能,歡迎大家升級體驗! ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...