扣丁書屋

iOS客戶端埋點的前世今生

零、簡介

本篇文章將結合狐友iOS客戶端埋點的實踐,給大家介紹不同的埋點實現方案及一些問題總結。

主要包括以下內容:

  • 初步認識埋點概念及分類

  • 服務端埋點

  • 客戶端埋點

  • 客戶端埋點的實現

  • 全代碼埋點實現

  • 無侵入埋點實現

  • 一些問題的總結

一、認識埋點

所謂埋點是數據領域的專業術語,也是互聯網應用里面的一個俗稱。埋點的學名應該是事件追蹤,對應的英文是Event Tracking;它是對特定用戶行為或事件進行捕獲,然后進行處理、發送的技術及其實施過程的統稱。

埋點分類

服務端埋點

在服務器端做數據收集,在用戶請求服務器的關鍵業務處添加埋點。

優點:實時收集數據,數據準確、不存在延時上報問題;埋點發生在服務器端,因此埋點需求改變時只需要在服務端更改就好了,能夠實時滿足埋點需求變更的要求。

缺點:如果用戶的行為及事件不涉及到服務端網絡請求,則服務端收集不到數據;如果客戶端沒有聯網,則服務端也收集不到數據。

客戶端埋點

由手機客戶端或者Web網頁端收集、記錄用戶的行為數據并進行上報。

優點:由于客戶端是直接面向用戶的,所以能夠比較直接、全面的收集用戶的行為數據,可以收集不涉及服務器請求的數據。

缺點:收集的數據需要通過網絡上傳至服務器,需要處理各種網絡異常情況,容易造成數據漏報;更改、新增埋點需求則需要發布新版本客戶端。

經過簡單的介紹,相信大家對什么是埋點有個初步的了解;下面以狐友iOS客戶端埋點的實踐為例,給大家介紹一下客戶端埋點的實現及遇到的一些問題的解決辦法。

二、客戶端埋點實現

埋點實現的三板斧

目前,iOS 開發中常見的埋點方式,主要包括代碼埋點、無侵入埋點和可視化埋點這三種;

  • 代碼埋點:客戶端開發,在業務代碼中直接添加埋點代碼的方式,就是代碼埋點

    這種方式能很精確的,在需要埋點的業務代碼處加上埋點代碼;可以很方便的記錄當前環境的變量值、方便調試、并跟蹤埋點內容;但是存在開發工作量大、埋點代碼到處都是、后期難以維護等問題。

  • 無侵入埋點:并不是不需要埋點,而更確切地說是"全埋點",埋點代碼不會出現在業務代碼中,便于管理和維護

    它的缺點在于埋點成本高,后期的解析也比較復雜,再加上 view_path 的不確定性;這種方案并不能解決所有的埋點需求,但對于大量通用的埋點需求來說,能夠節省大量的開發和維護成本。

  • 可視化埋點:這種埋點方式結合無侵入埋點,將埋點需求的生成過程可視化;比如允許開發以外的同學,通過可視化的界面、按照既定的規則生成埋點需求,將這些需求通過服務器下發給手機客戶端;手機客戶端解析埋點需求,然后將需要的埋點數據上報。

    這種方式將埋點的增加和修改的工作可視化,提升了增加埋點的體驗;

    但是缺點也很明顯,前期工作量巨大;埋點生成規則、解析規則需要生成端及客戶端實時同步,否則生成的埋點需求客戶端不能解析;添加埋點的同學需要熟悉整個思路及過程,以便能夠獨立的添加埋點需求。

在上面的埋點方式中,可視化埋點和無埋點,都屬于是無侵入的埋點方案,因為它們都不需要在工程代碼中寫入埋點代碼。所以,采用這樣的無侵入埋點方案,既可以做到埋點被統一維護,又可以實現和工程代碼的解耦(有好有壞、后面會有介紹)。

現在我們對客戶端的埋點方式有了初步了解,下面我們以相對簡單、直接的代碼埋點為例進行簡單實現;在著手實現之前,我們根據常見的業務需求,將埋點上報的數據按需進行分類。

埋點數據分類

埋點數據的分類應該根據實際的需求而定,但一般需要統計的數據都包括頁面的曝光、頁面中某個元素的曝光及用戶的點擊事件曝光等,所以這里我們將埋點數據進行如下分類:

PageView:頁面曝光類型埋點

針對頁面曝光,當APP內容從當前頁面進入到新的頁面時,則針對新的頁面進行埋點上報;

上報時機:當進入新的頁面就上報這個埋點

上報數據內容(根據實際情況,不一定每一項數據都需要上報):

pageId(當前頁面id),
contentId(當前頁面展示的內容id),
sourcePage(來源頁面即上一個頁面),
sourceClick(上一個頁面的點擊位置),
....

ViewElement:視圖元素類型埋點

針對頁面中某個視圖的曝光,比如功能頁的某個引導氣泡、視圖刷新次數、列表頁的cell展示曝光等;

上報時機:

普通頁面元素,上報時機為目標視圖元素出現的時候;如果是列表中cell曝光,則可能是退出當前列表頁、加載新數據、進入到新頁面的時候上報這些數據;

上報數據內容(根據實際情況,不一定每一項數據都需要上報):

ViewElementId(當前視圖id),
contentId(當前視圖展示的內容id),
activityIds(展示過的每個活動id),
....

ClickPositon:點擊類型埋點

這類埋點比較容易理解,就是當用戶點擊了某個按鈕或某個視圖的時候觸發的埋點上報;

上報時機:發生即上報;

上報數據內容(根據實際情況,不一定每一項數據都需要上報):

ClickPositonId(放生點擊按鈕或者視圖id),
contentId(當前視圖展示的內容id),
sourcePage(當前按鈕所在的頁面),
sourceClick(當前按鈕所在頁面中的位置),
....

基礎數據結構的創建

基于前面的埋點數據分類,下面通過Swift實現基礎組件的搭建;我們主要處理PageView、ViewElement、ClickPosition這三類埋點的上報,結合Swift中Enum的特性,我們有下面的實現:

public enum Events {
    ///頁面曝光埋點
    case pageView(key: PageKey, properties: [PageKey.Property])
    ///頁面元素曝光埋點
    case viewElement(key: ViewElementKey, properties: [ViewElementKey.Property])
    ///按鈕點擊事件曝光埋點
    case clickPosition(key: ClickPositionKey, properties: [ClickPositionKey.Property])
}

//MARK: -PageView 頁面埋點的事件及對應的參數
public extension Events {
    // 用來標記不同的頁面
    enum PageKey: Int32 {
        case Other = 0
        case page1 = 1
        case page2 = 2
        // ...

        public enum Property {
            // 在前一個頁面點擊哪個位置進入的當前頁面
            case sourceClick(id: SourceClick)
            // 從哪個頁面進入的當前頁面
            case sourcePage(id: SourcePage)
            case content(_ content: String)
            // ...
        }
    }
}

//MARK: -ViewElement 頁面元素刷新埋點的事件及對應的參數
public extension Events {
    // 用來區分需要上報的不同的頁面元素
    enum ViewElementKey: Int32 {
        case Other = 0
        case element1 = 1
        case element2 = 2
        // ...

        public enum Property {
            case feedidList(_ feedIds: [String])
            case beUids(_ ids: [String])
            case activityIds(_ ids: [String])
            // ...
        }
    }
}

//MARK: -ClickPosition 按鈕點擊事件的埋點及參數
public  extension Events {
    // 用來區分不同的點擊位置
    enum ClickPositionKey: Int32 {
        case Other = 0
        case position1 = 1
        case position2 = 2
        // ...

        public enum Property {
            case followName(_ flowName: FlowName)
            case circleName(_ circleName: String)
            case status(_ statu: Status)
            // ...
        }
    }
}

public extension Events {
    enum SourcePage: Int32 {
        case Other = 0
        case page1 = 1
        case page2 = 2
        // ...
    }

    enum SourceClick: Int32 {
        case Other = 0
        case source1 = 1
        case source2 = 2
        // ...
    }

    enum Status: Int32 {
        case Other = 0
        /// 1
        case Success = 1
        /// 2
        case Fail = 2
    }
    // ...
}

需要上報的基礎數據結構我們已經建立完成,下面我們就可以上報這些數據了,為了更加的Swift化,我們通過協議來封裝我們的接口及實際上報內容的數據組成實現;

/// 埋點上報的協議
/// 遵守協議后可在遵守了該協議類型的類方法或實例方法中直接調用trackEvent(:)、或track(:)方法上報埋點
/// 這兩個方法在協議的擴展內提供了默認實現
public protocol EventsProtocol {
    ///該方法已經提供默認實現,不需要遵守者自己實現, 同樣的還提供了一個實例方法
    static func trackEvent(_ types: Events)

    ///該方法已經提供默認實現,不需要遵守者自己實現, 同樣的還提供了一個static方法
    func track(_ event: Events)
}

public extension EventsProtocol {
    func track(_ event: Events) {
        Self.processTrack(event: event)
    }

    static func trackEvent(_ types: Events) {
        processTrack(event: types)
    }
}

private extension EventsProtocol {
    // 處理獲得的上報數據
    static func processTrack(event: Events) {
        switch event {
        case let .pageView(key: pageId, properties: properties):
            // processPageViewTrack(pageId, with: properties)

        case let .viewElement(key: viewElementId, properties: properties):
            // processViewElementTrack(viewElementId, with: properties)

        case let .clickPosition(key: clickPositionId, properties: properties):
            // processClickPositionTrack(clickPositionId, with: properties)
        }
    }
}

應用舉例

有了上面這些骨架,實際上我們就可以在工程中進行埋點應用了,以添加一個詳情頁面的PageView及ClickPosition類型埋點為例:

?注意:在類方法和實例方法需要分別調用相對應的方法進行埋點

class DetailController: UIViewController, EventsProtocol {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 上報頁面曝光埋點
        track(.pageView(key: .page1, properties: [.content("某某詳情頁曝光了"), .sourcePage(id: .page2)]))
    }

    // 關注按鈕點擊
    @objc func followingButtonClick() {
        track(.clickPosition(key: .position1, properties: [.content("關注用戶id:userId"), .sourcePage(id: .page1)]))
    }

    // 類方法發生埋點上報
    static func otherViewClick() {
        trackEvent(.clickPosition(id: .position1, properties: [.content("其他點擊事件")]))
    }
}

現在有一個問題,在我們的PageView類型埋點中,需要上報的參數sourcePage(來源頁面)是需要從前一個頁面傳遞過來的,類似這種需要從前一個頁面傳遞到當前頁面,從而上報的數據是一個我們需要解決的問題;為此,我們設計了新的通過單向鏈表結構傳遞數據的方式,來解決這個問題。

通過單向鏈表結構傳遞數據

基本實現

從前一個頁面向當前頁面傳遞數據辦法有很多,比如在初始化方法中添加傳遞數據的參數、單獨增加一個字典屬性用來傳遞參數等等辦法。

下面給大家介紹一種通過單向鏈表式結構傳遞數據的方式TrackChain;

圖片思路:如上圖所示,我們創建一個Chain,它有一個property用來存儲需要傳遞到下一個頁面的屬性,同時有一個parentChain也是一個Chain用來指向parent,以便在需要的時候獲得parentChainproperty;

實現代碼如下:

class Chain: NSObject {
    var property: [AnyHashable: Any]?
    var parent: Chain?

    init(property: [AnyHashable: Any]? = nil, parent: Chain? = nil) {
        self.property = property
        self.parent = parent
    }

    func add(property: [AnyHashable: Any]) {
        guard var _ = self.property else {
            self.property = property
            return
        }
        for (key,value) in property {
            self.property?.updateValue(value, forKey: key)
        }
    }
}

extension Chain {
    static var responsibilityChainTable = [ObjectIdentifier: Chain]()
}

有了我們的數據鏈表,下面我們對鏈表進行管理:

protocol Trackable : class {   
}

extension Trackable {
// 初始化Chain的方法,每個頁面的每個id對應一個chain,存儲在 responsibilityChainTable 中
   func setupTrackableChain(trackedProperties: [AnyHashable: Any] = [:], parent: Trackable? = nil) {

       let setupClosure: () -> Void = {
           var parentChain: Chain? = nil
           if let parentId = parent?.uniqueIdentifier,
              let chain = Chain.responsibilityChainTable[parentId] {
               parentChain = chain
           }

           let identifier = self.uniqueIdentifier
           // 已經存在更新原有的,沒有則創建新的并進行存儲
           if let existedChain = Chain.responsibilityChainTable[identifier] {
               existedChain.parent = parentChain
               existedChain.add(property: trackedProperties)
           }else {
               let newChain = Chain(property: trackedProperties, parent: parentChain)
               Chain.responsibilityChainTable[identifier] = newChain
           }
       }

       if Thread.isMainThread {
           setupClosure()
       } else {
           DispatchQueue.main.async {
               setupClosure()
           }
       }
   }
   // 向已有的chain中追加屬性,如果不存當前id不存在對應的chain,則會順便創建新的chain
   func add(trackable properties: [AnyHashable: Any]) {
       let identifier = self.uniqueIdentifier
       // 已經存在更新原有的,沒有則創建新的并進行存儲
       if let existedChain = Chain.responsibilityChainTable[identifier] {
           existedChain.add(property: properties)
       }else {
           let newChain = Chain(property: properties, parent: nil)
           Chain.responsibilityChainTable[identifier] = newChain
       }
   }
   // 從當前chain 的 parentChain 中獲取從父向子傳遞的 property
   func getPropertyFromParentChain(for key: AnyHashable) -> Any?{
       let identifier = self.uniqueIdentifier
       guard let existedChain = Chain.responsibilityChainTable[identifier] else {
           return nil
       }
       return existedChain.parent?.property?[key]
   }

   // 必要時候清理關聯數據
// 比如頁面銷毀的時候需要清理掉當前頁面存儲在 responsibilityChainTable 中的數據
   func cleanTrackable() {
       let identifier = self.uniqueIdentifier
       guard let existedChain = Chain.responsibilityChainTable[identifier] else {
           return
       }
       existedChain.parent = nil
       existedChain.property = nil
       Chain.responsibilityChainTable.removeValue(forKey: identifier)
   }
}

extension Trackable {
// 給遵守Trackable的類默認生成唯一標識id
   var uniqueIdentifier: ObjectIdentifier {
       return ObjectIdentifier(self)
   }
}

應用舉例

下面就是具體的應用了,重新回到我們的頁面曝光埋點我們進行如下更改:

class ListController: UIViewController {
    deinit {
        // 當控制器銷毀后,清理存儲在 responsibilityChainTable 中的數據
        cleanTrackable()
    }
    func pushToDetailController() {
        // 添加屬性到current chain
        add(trackable: ["sourcePage": "sourcePage1"])

        let detail = DetailController()
        // 初始 detail chain  并賦值
        detail.setupTrackableChain(parent: self)

        navigationController?.pushViewController(detail, animated: true)
    }
}
// 遵守協議
extension ListController: Trackable { }

class DetailController: UIViewController, EventsProtocol {
    deinit {
        // 當控制器銷毀后,清理存儲在 responsibilityChainTable 中的數據
        cleanTrackable()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let sourcePage = getPropertyFromParentChain(for: "sourcePage")
        // 上報頁面曝光埋點
        track(.pageView(key: .page1, properties: [.content("\(sourcePage)"), .sourcePage(id: .page2)]))
    }

    // 關注按鈕點擊
    @objc func followingButtonClick() {
        track(.clickPosition(key: .position1, properties: [.content("關注用戶id:userId"), .sourcePage(id: .page1)]))
    }
}

上面只是簡單的實現,更進一步可以將Chain.property中存儲的數據和埋點需要上報的參數類型關聯起來,這樣存取屬性的時候會更加的方便;這里只是提供一種頁面間傳遞數據的思路供大家參考,還需要具體情況具體分析;

遇到的問題

理想是豐滿的,現實是骨感的,我們設計了單向鏈表Chain來解決數據的跨頁面傳遞問題,但是在實際的應用中發現了一些問題,下面就遇到的問題簡單舉例:

1、復雜頁面中存儲數據到對應的Chain中的問題

假設頁面A頁面B傳遞數據,整個過程如下:

  • 頁面A中初始化頁面A對應的ChainA,將需要傳遞的數據存儲在ChianAproperty
  • 在合適的位置初始化頁面B對應的ChainB,將ChainA作為ChainBparentChain
  • 頁面B中需要上報埋點數據時,通過ChainBChainAproperty中取出數據,完成上報

如果一切按照預想的話,整個流程如上所述;

但是當我們的頁面A是一個比較復雜的頁面,且需要存儲到ChainA中的數據發生在不同子視圖時,這時候我們一定要注意不同子視圖調用的ChainA一定要是同一個(即頁面A本身創建的ChainA),不然容易造成數據的缺失且問題不容易排查 。

2、頁面銷毀時數據清理問題,即調用cleanTrackable()的問題

為了清理數據,我們需要在頁面銷毀的時候需要調用cleanTrackable()方法,通過當前頁面的uniqueIdentifier清理responsibilityChainTable中存儲的對應的數據;

續接上面的問題,當頁面A的子視圖銷毀時不能銷毀頁面A對應的ChainA,只有在頁面A銷毀時,才能去銷毀對應的ChainA。如果不注意這里容易產生問題,特別是頁面A存在ChildViewController的情況下。

經過上面的步驟之后,我以代碼埋點的方式實現了埋點需求。但在我們滿足了產品的需求后,我們可能發現我們自己并沒有得到滿足,因為聽說還有無侵入埋點(全埋點)、可視化埋點;

三、無侵入埋點的實現探究

運行時方法替換方式實現無侵入埋點

無侵入埋點的實現思路,就是通過運行時方式Hook關鍵方法,在被我們Hook的方法中,添加我們原本應該寫在業務代碼中的埋點代碼。

前面我們已經了解了我們的埋點需求,大多數埋點都會被添加在固定的方法中,這些固定的方法就是我們Hook處理的目標。

下面我們先寫一個運行時替換方法的工具類,以便于我們用來Hook目標方法:

#import "MyHook.h"
#import <objc/runtime.h>

@implementation MyHook
+ (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
    Class class = classObject;
    // 得到被替換類的實例方法
    Method fromMethod = class_getInstanceMethod(class, fromSelector);
    // 得到替換類的實例方法
    Method toMethod = class_getInstanceMethod(class, toSelect or);

    // class_addMethod 返回成功表示被替換的方法沒實現,然后會通過 class_addMethod 方法先實現;
    // 返回失敗則表示被替換方法已存在,可以直接進行 IMP 指針交換
    if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
      // 進行方法的替換
        class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
    } else {
      // 交換 IMP 指針
        method_exchangeImplementations(fromMethod, toMethod);
    }
}

@end

頁面曝光無侵入埋點實現

前面我們在UIViewControllerviewWillAppear:方法中添加頁面曝光埋點,現在我們做無侵入埋點;

首先我們Hook UIViewControllerviewWillAppear:方法,然后在我們的hook_viewWillAppear:方法中進行埋點上報;

@implementation UIViewController (report)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 通過 @selector 獲得被替換和替換方法的 SEL
        // 作為 MyHook:hookClass:fromeSelector:toSelector 的參數傳入
        SEL fromSelectorAppear = @selector(viewWillAppear:);
        SEL toSelectorAppear = @selector(hook_viewWillAppear:);
        [MyHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
    });
}

- (void)hook_viewWillAppear:(BOOL)animated {
    // 先執行插入代碼,再執行原 viewWillAppear 方法
    [self reportViewWillAppear];
    [self hook_viewWillAppear:animated];
}

- (void)reportViewWillAppear {
  // 識別具體controller 進行pageView的埋點上報
}

@end

上面就是我們無侵入方式上報PageView類型埋點的實現;

點擊類型事件的無侵入埋點實現

按鈕點擊、cell點擊、視圖點擊等ClickPosition類型的埋點我們也可以通過Hook方法來實現無侵入埋點;

同樣的思路我們可以對以下方法進行Hook處理(部分方法,更多可以參考DiDiPrism的實現):

@implementation UIControl (report)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [MyHook hookClass:[self class] fromSelector:@selector(sendAction:to:forEvent:) toSelector:@selector(hook_sendAction:to:forEvent:)];
    });
}
@end

@implementation UIView (report)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [MyHook hookClass:[self class] fromSelector:@selector(touchesEnded:withEvent:) toSelector:@selector(hook_touchesEnded:withEvent:)];
    });
}
@end

上面我們通過Hook方法完成對應的埋點事件上報,除了 UIViewController、UIButton 控件以外,Cocoa 框架的其他控件都可以使用這種方法來進行無侵入埋點。以 Cocoa 框架中最復雜的 UITableView 控件為例,可以使用 hook setDelegate 方法來實現無侵入埋點。另外,對于 Cocoa 框架中的手勢事件(Gesture Event),我們也可以通過 hook initWithTarget:action: 方法來實現無侵入埋點;

無侵入埋點的實際應用

我們已經了解到無侵入埋點的實現思路,現在我們嘗試用無侵入埋點替換我們的全代碼埋點;

以PageView類型埋點為例,我們需要Hook特定方法,然后在Hook方法中進行埋點上報。

Hook方法除了用上面介紹過的自己實現外,還可以通過成熟的三方庫來實現,這里我們通過Aspects完成Hook工作:

@implementation AspectHandle
- (void)setup {
    [[UIViewController class]
     aspect_hookSelector: @selector(viewWillAppear:)
     withOptions: AspectPositionBefore
     usingBlock: ^(id<AspectInfo> aspectInfo) {
        id controller = aspectInfo.instance;
        id arguments = aspectInfo.arguments;
        // 1 更進一步處理

    } error:NULL];
}
@end

目標方法Hook完成后,我們就可以在1處做進一步的識別對象、上報數據等處理了。Aspects能Hook系統方法外,也同樣能Hook開發者自定義的方法,具體方法同上;

除此之外Aspects也能Hook Swift方法,但如果是自定義的Swift類及方法需要做一些特別的處理:

  • 需要Hook 的目標必須是類,且需繼承自NSObject
  • 需要Hook的目標的方法必須經過 @objc dynamic 修飾
  • 需要注意Swift 類名前面是有命名空間的

上面就是Hook自定義Swift方法時的一些注意事項,就不做代碼舉例了;

經過一段時間的實踐后,發現無侵入埋點遠沒有想象中的那么完美,下面就無侵入埋點實踐的過程中碰到的問題做一個簡單的總結;

實踐過程中遇到的問題

1、怎樣去識別當前控制器是哪一個控制器,以便上報不同的PageId?

答: 在Hook方法中,我們可以獲取到當前的控制器對象,但是識別出當前控制器具體是哪個業務控制器是一個重點及難點,因為我們需要根據不同的業務控制器上報不同的埋點數據;

現在業界比較成熟的做法是,按照一定規則生成一個盡可能唯一的標識來識別控制器,比如我們可以按照下面的策略生成一個控制器的標識:

id = navigationController類名(nullable)+parentController類名(nullable)+controller類名+title

最終每個控制器都會有一個類似的ID:

id = "BaseNavigationController_DetailController_用戶詳情頁"

根據上面的辦法我們應該能識別絕大多數的控制器頁面,如果有特殊的情況不能識別的我們可能需要進一步的完善我們的標識生成策略,以便讓最終的id盡可能的唯一;

2、怎樣去識別是哪個按鈕或者視圖?

答:解決這個問題的方案同前面識別控制器的方案類似,同樣的我們需要給Button等視圖生成一個能夠識別的唯一標識,現在比較成熟的方案通過獲得視圖所處的樹形結構及本身屬性等生成標識:

id = UIWindow_SuperSuperView···_SuperView_brotherCount_currentViewIndex_action選擇器名_視圖類名_title

3、怎樣獲得和業務相關的上報參數?比如DetailController上報PageView的埋點時,同時需要上報 sourcePage等參數;

答:目前來說這類問題在無侵入埋點方案中沒有好的解決辦法,這里我們通過借助前面介紹的跨頁面傳遞參數Chain機制來協助解決這個問題。

我們將需要的參數存儲到Chain中,然后在hook_viewWillAppear方法中取出;

class ListController: UIViewController {
    func pushToDetailController() {
        // 添加屬性到current chain
        add(trackable: ["sourcePage": "sourcePage1"])
        let detail = DetailController()
        // 初始 detail chain  并賦值
        detail.setupTrackableChain(parent: self)
        navigationController?.pushViewController(detail, animated: true)
    }
}

....
func hook_ViewWillAppear {
  // 根據標識,識別具體controller獲得pageid

  // 通過chain 獲得detailId
   detailId = getPropertyFromParentChain(for: "獲得detailId")
   track(.pageView(key: pageid, properties: [.content("\(detailId)"), .sourcePage(id: .page2)]))
}

上面是一些偽代碼,主要是給大家提供一個解決問題的思路。雖然問題得到了解決,但是現在也已經不能算是無侵入埋點了,因為需要我們在必要的地方插入代碼進行數據的傳遞;

4、如果我們Hook了自定義的方法,那么如果這個方法名字被更改了怎么辦?

這類問題在實踐過程中,沒有找到一個能一定解決或者避免的辦法,但是可以通過下面的方法做到盡量避免:

  • 添加Hook代碼的同學,給被Hook方法添加備注,比如明確備注這個方法被Hook處理,如果有更改請通知
  • 在Hook工具類中,Hook方法失敗時添加容易識別的打印日志,以便來提醒大家
  • 給Hook的類和方法添加測試,測試的目標可以是去判斷類或方法是否存在或被更改;通過腳本定期的run這些測試,如果出錯的給予郵件提醒

5、無侵入埋點開發過程中,多名開發同學之間怎樣相互配合?

這類問題的解決也沒有一定之規,只能是去盡量避免,下面是一些小的建議:

  • 代碼的注釋要清楚、明確
  • 形成標準有效的文檔,大家都要熟悉文檔內容、按照文檔規范操作;并且做到及時更新,更新后及時同步給大家
  • 每個人都要熟悉無侵入埋點的埋點的方案、及對應問題的解決辦法

6、無侵入埋點方案能夠完全滿足我們實際的埋點需求嗎?

在實際的開發過程中我們會發現,埋點需求的復雜度會隨著業務復雜度的增加而增加,就可能會碰到無侵入埋點方案無法解決的問題。這時我們逼不得已就會做一些妥協,在代碼中直接插入埋點代碼,或者插入一些輔助代碼,無侵入埋點在這個時候就破功了。

五、總結

恭喜,文章到此結束

以上就是狐友iOS客戶端埋點功能的實現及一些問題的總結,希望能夠給同樣有埋點需求的同學一些啟發和借鑒。

希望閱讀完本篇文章后,您能夠對埋點概念及分類有初步的了解、對客戶端埋點的不同實現方案有了較深入的認識;

客戶端埋點實現方案有多種,每種方案各有利弊;在選擇實現方案時需要根據實際情況、結合需求綜合判斷,沒有一定之規。

在合適的位置用合適的辦法,道路千萬條、適用第一條。


https://mp.weixin.qq.com/s?__biz=MzA5NzMwODI0MA==&mid=2647771096&idx=1&sn=7efbb12531db9a39e9ac1a7a57f7be42&chksm=8887bce7bff035f10f296bd0ef6dab62eefa5f617100c81217242b12f94f7d1a4117cf6a7b0c&mpshare=1&scene=1&srcid=1119w7YA2aVCgIh5yGIQip6I&sharer_sharetime=1637326178296&sharer_shareid=7f2af9604fbdc84c4e358e57e738e84a#rd

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 8月以前  |  18055次閱讀
快速配置 Sign In with Apple 2年以前  |  5484次閱讀
APP適配iOS11 3年以前  |  4436次閱讀
App Store 審核指南[2017年最新版本] 3年以前  |  4262次閱讀
所有iPhone設備尺寸匯總 3年以前  |  4184次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  3916次閱讀
開篇 關于iOS越獄開發 3年以前  |  3794次閱讀
在越獄的iPhone設置上使用lldb調試 3年以前  |  3719次閱讀
給數組NSMutableArray排序 3年以前  |  3642次閱讀
使用ssh訪問越獄iPhone的兩種方式 3年以前  |  3346次閱讀
UITableViewCell高亮效果實現 3年以前  |  3344次閱讀
關于Xcode不能打印崩潰日志 3年以前  |  3242次閱讀
使用ssh 訪問越獄iPhone的兩種方式 3年以前  |  3083次閱讀
為對象添加一個釋放時觸發的block 3年以前  |  2857次閱讀
使用最高權限操作iPhone手機 3年以前  |  2828次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>