用戶行為你看我就夠了(hook)

目錄

<a name="前言"></a>前言

用戶行為的統(tǒng)計(jì)可以幫助我們更好的了解用戶的各方面信息。現(xiàn)在比較主流的有兩款三方統(tǒng)計(jì)庫(kù):友盟(國(guó)內(nèi))和flurry(國(guó)外)。但是用戶行為收集的代碼往往分散在各個(gè)類中,難以維護(hù)也不雅觀,今天交流一種比較好用的用戶行為統(tǒng)計(jì)方法。本文將全部采用swift來解析。

<a name="準(zhǔn)備工作"></a>準(zhǔn)備工作

  • 三方庫(kù)

我們需要兩個(gè)三方庫(kù):友盟和Aspects.
* 友盟用來做最終的統(tǒng)計(jì)收集
* Aspects 是本文的關(guān)鍵點(diǎn),它hook住了我們想要的方法,高度集中。

我們采用cocoapods 來管理三方庫(kù)

platform :ios, '8.0'
use_frameworks!

target 'UserBehaviorSwift' do
    #hook
    pod 'Aspects', '~> 1.4.1'
    #友盟統(tǒng)計(jì)(錯(cuò)誤分析,事件統(tǒng)計(jì))
    pod 'UMengAnalytics-NO-IDFA', '~> 4.0.5'
end

  • 橋接(如果是oc項(xiàng)目可以跳過該項(xiàng))

因這兩個(gè)庫(kù)都是oc寫的,故我們需要新建一個(gè)bribing文件。命名格式最好按照官方建議的 xxx-Bridging-Header.h。記得勾選上targets。如下圖

橋接oc頭文件.png

橋接文件還需要配置路徑


橋接文件路徑.png

有些三方庫(kù)的head search(不是所有的庫(kù)都需要,今天這兩個(gè)庫(kù)中Aspects需要配置)


橋接文件時(shí)必要配置的三方庫(kù).png

橋接代碼:

#ifndef UserBehaviorSwift_Bridging_Header_h
#define UserBehaviorSwift_Bridging_Header_h

// 這個(gè)庫(kù)不在 headSearch里面設(shè)置就找不到。單獨(dú)加上
#import "Aspects.h"

//友盟不設(shè)置就能找到,應(yīng)該是framework的緣故
#import <UMMobClick/MobClick.h>

#import <UMMobClick/MobClickSocialAnalytics.h>

#endif /* UserBehaviorSwift_Bridging_Header_h */
  • 代碼準(zhǔn)備

我們需要一個(gè)RootVC類和兩個(gè)繼承與RootVC 的A 和 B;一個(gè)RootButton.整個(gè)項(xiàng)目的代碼都已經(jīng)傳到我的github,地址在文章的結(jié)尾。


需要的類.png

<a name="頁(yè)面的hook"></a>頁(yè)面的hook

  • 一般的用戶行為收集的寫法都是直接在對(duì)應(yīng)的類中寫業(yè)務(wù)。類越多,就寫的越多。太過麻煩,難以維護(hù)。
    override func viewWillAppear(_ animated: Bool) {
        MobClick.beginLogPageView("falgA")
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        MobClick.endLogPageView("falgA")
    }
  • 我們?cè)趺磳懀靠梢杂肁spect 巧妙的hook住vc 的生命周期函數(shù)。拿viewWillAppear來舉例。
          //進(jìn)入頁(yè)面
        let viewWillAppearBlock:@convention(block) (AspectInfo)-> Void = { aspectInfo in
            
            //獲得實(shí)例
            let instance = aspectInfo.instance() as? RootViewController
            guard instance != nil else {
                return
            }
            
            //實(shí)例轉(zhuǎn) 格式化 string
            let className = String.init(reflecting: instance)
            let arr = className.components(separatedBy: ".")
            guard arr.count > 1 else {
                return
            }
            
            //最終類名
            let finalClassName = arr[1].components(separatedBy: ":").first! as String
            
            //標(biāo)題
            let title = instance?.title != nil ? instance?.title : "unknown"
            
            
            //最終使用 : type + 類名 + 標(biāo)題 ,用“/”來分割
            let logFlag = "pageIn/" + finalClassName + "/" + title!
            MobClick.beginLogPageView(logFlag)
            print(logFlag)
        }
        
        //轉(zhuǎn)換
        let viewWillAppearBlockWrapped: AnyObject = unsafeBitCast(viewWillAppearBlock, to: AnyObject.self)
        
        //最終hook住對(duì)應(yīng)的函數(shù),這里設(shè)置了AspectOptions.positionBefore模式,會(huì)在viewWillAppear 即將被調(diào)用前調(diào)用
        do {
            try RootViewController.aspect_hook(#selector(RootViewController.viewWillAppear(_:)), with: AspectOptions.positionBefore, usingBlock: viewWillAppearBlockWrapped)
        }
        catch {
            print(error)
        }

解析

用RootVC hook 住viewWillAppear,所有繼承與RootVC的類在每次的viewWillAppear被調(diào)用之前都會(huì)調(diào)用我么已經(jīng)準(zhǔn)備好的block。因是swift 調(diào)用oc 故用convention修飾,這里如果直接使用閉包會(huì) crash。block里會(huì)返還一個(gè)當(dāng)前調(diào)用者的實(shí)例,用實(shí)例我們可以獲得 類名、標(biāo)題。我們?cè)O(shè)定了格式來確保這個(gè)flag的唯一性質(zhì) :進(jìn)入頁(yè)面("pageIn/" + finalClassName + "/" + title),離開頁(yè)面("pageOut/" + finalClassName + "/" + title)。最后我們使用 MobClick.beginLogPageView(logFlag) 對(duì)其行為進(jìn)行收集。

<a name="按鈕的hook"></a>按鈕的hook

按鈕的hook相對(duì)于頁(yè)面來說會(huì)復(fù)雜一點(diǎn),多了一些步驟。

          //按鈕
        let touchesBeganBlock:@convention(block) (AspectInfo)-> Void = { aspectInfo in
            let instance = aspectInfo.instance() as? RootButton
            
            guard instance != nil else {
                return
            }
            
            let className = String.init(reflecting: instance?.allTargets.first)
            let arr = className.components(separatedBy: ".")
            guard arr.count > 1 else {
                return
            }
            
            let finalClassName = arr[1].components(separatedBy: ":").first! as String
            
            let title = instance?.titleLabel?.text != nil ? instance?.titleLabel?.text : "unknown"

            let logFlag = "event/" + finalClassName + "/" + title!
            
            let path = Bundle.main.path(forResource: "UserBehavior", ofType: "plist")
            let dict = NSDictionary.init(contentsOfFile: path!)
            let id = dict?.object(forKey: logFlag) as? String
            guard id != nil else {
                return
            }
            
            MobClick.event(id, label: logFlag)

            print(logFlag)
        }

解析

相對(duì)于類來說,按鈕需要通過alltargets 的一系列格式化才能得到類名。最終格式為("event/" + finalClassName + "/" + title)。因MobClick.event 事件一般都需要產(chǎn)品經(jīng)理 給你一套他們定制的id,假如你反駁不了的話,那么就好好的享受吧!這里用了plist進(jìn)行管理,把所有的按鈕和對(duì)應(yīng)的id都寫進(jìn)去,用的時(shí)候再取出來。

注意: 我么在解析的時(shí)候可能碰到一些沒有標(biāo)題或者其他的情況,所有要嚴(yán)格的進(jìn)行校驗(yàn),寧愿少記錄一條都礙事,也要避免crash.

plist如下圖所示

plist.png

效果圖

最終效果.gif

<a name="總結(jié)"></a>總結(jié)

實(shí)際開發(fā)中可能不僅僅需要到 頁(yè)面和按鈕這兩種,但都是一樣的道理,大多都可以通過這種方法來寫,個(gè)別寫不了的就直接用原始方法寫也是無傷大礙。所有的代碼都已經(jīng)傳到Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容