落地西瓜視頻埋點方案,埋點從未如此簡單

點贊關(guān)注,不再迷路,你的支持對我意義重大!

?? Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收錄,這里有 Android 進階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長。(聯(lián)系方式 & 入群方式在 GitHub)

前言

  • 目前,幾乎每個商用應(yīng)用都有數(shù)據(jù)埋點的需求。你的 App 是怎么做埋點的呢,有遇到讓你 “難頂” 的問題嗎?
  • 在這篇文章里,我將帶你建立數(shù)據(jù)埋點的基本認(rèn)識,還會介紹西瓜視頻團隊的前端埋點方案,最后為你帶來我的落地實現(xiàn) EasyTrack。如果能幫上忙,請務(wù)必點贊加關(guān)注,這真的對我非常重要。

目錄


1. 數(shù)據(jù)埋點概述

1.1 為什么要埋點?

“除了上帝,任何人都必須用數(shù)據(jù)說話”,在數(shù)據(jù)時代,使用數(shù)據(jù)驅(qū)動產(chǎn)品迭代已經(jīng)稱為行業(yè)共識。在分析應(yīng)用數(shù)據(jù)之前,首先需要獲得數(shù)據(jù),這就需要前端或服務(wù)端進行數(shù)據(jù)埋點。

1.2 數(shù)據(jù)需求的工作流程

首先,你需要了解數(shù)據(jù)需求的工作流程,需求是如何產(chǎn)生,又是如何流轉(zhuǎn)的,主要分為以下幾個環(huán)節(jié):

  • 1、需求產(chǎn)生: 產(chǎn)品需求引起產(chǎn)品形態(tài)變化,產(chǎn)生新的數(shù)據(jù)需求;
  • 2、事件設(shè)計: 數(shù)據(jù)產(chǎn)品設(shè)計埋點事件并更新數(shù)據(jù)字典文檔,提出埋點評審;
  • 3、埋點開發(fā): 開發(fā)進行數(shù)據(jù)埋點開發(fā);
  • 4、埋點測試: 測試進行數(shù)據(jù)埋點測試,確保數(shù)據(jù)質(zhì)量;
  • 5、數(shù)據(jù)消費: 數(shù)據(jù)分析師進行數(shù)據(jù)分析,推薦系統(tǒng)工程師進行模型訓(xùn)練,賦能產(chǎn)品運營決策。

1.3 數(shù)據(jù)消費的經(jīng)典場景

消費場景 需求描述 技術(shù)需求
滲透率分析 統(tǒng)計 DAU/PV/UV/VV 等 準(zhǔn)確的上報時機
歸因分析 分析前因后果 準(zhǔn)確上報上下文 (如場景、會話、來源頁面)
1. A / B 測試
2. 個性化推薦
分析用戶特征、產(chǎn)品特征等 準(zhǔn)確上報事件屬性

可以看到,在歸因分析中,除了需要上報事件本身的屬性之外,還需要上報事件產(chǎn)生時的上下文信息,例如當(dāng)前頁面、來源頁面、會話等。

1.4 埋點數(shù)據(jù)采集的基本模型

數(shù)據(jù)采集是指在前端或服務(wù)端收集需要上報的事件屬性的過程。為了滿足復(fù)雜、高效的數(shù)據(jù)消費需求,需要科學(xué)合理地設(shè)計端側(cè)的數(shù)據(jù)采集邏輯,基本可以總結(jié)為 “4W + 1H” 模型:

模型 描述 舉例
1、WHAT 什么行為 事件名
2、WHEN 行為產(chǎn)生的時間 時間戳
3、WHO 行為產(chǎn)生的對象 對象唯一標(biāo)識 (例如用戶 ID、設(shè)備 ID)
4、WHERE 行為產(chǎn)生的環(huán)境 設(shè)備所處的環(huán)境 (例如 IP、操作系統(tǒng)、網(wǎng)絡(luò))
5、HOW 行為的特征 上下文信息 (例如當(dāng)前頁面、來源頁面、會話)

2. 如何實現(xiàn)數(shù)據(jù)埋點?

2.1 埋點方案總結(jié)

目前,業(yè)界已經(jīng)存在多種埋點方案,主要分為全埋點、前端代碼埋點和服務(wù)端代碼埋點三種,優(yōu)缺點和適用場景總結(jié)如下:

全埋點 前端埋點 服務(wù)端埋點
優(yōu)勢 開發(fā)成本低 完整采集上下文信息 不依賴于前端版本
劣勢 數(shù)據(jù)量大,無法獲取上下文數(shù)據(jù),數(shù)據(jù)質(zhì)量低 前端開發(fā)成本較高 服務(wù)端開發(fā)成本較高、獲取上下文信息依賴于接口傳值
適用場景 通用基礎(chǔ)事件(如啟動/退出、瀏覽、點擊) 核心業(yè)務(wù)流程(如登錄、注冊、收藏、購買) 核心業(yè)務(wù)結(jié)果事件(如支付成功)
  • 1、全埋點: 指通過編譯時插樁、運行時動態(tài)代理等 AOP 手段實現(xiàn)自動埋點和上報,無須開發(fā)者手動進行埋點,因此也稱為 “無埋點”;

  • 2、前端埋點: 指前端 (包括客戶端) 開發(fā)者手動編碼實現(xiàn)埋點,雖然可以通過埋點工具或者腳本簡化埋點開發(fā)工作,但總體上還是需要手動操作;

  • 3、服務(wù)端埋點: 指服務(wù)端手動編碼實現(xiàn)埋點,缺點是需要客戶端需要侵入接口來保留上下文參數(shù)。

2.2 全埋點方案的局限性

表面上看,全埋點方案的優(yōu)勢很明顯:客戶端和服務(wù)端只需要一次開發(fā),就能實現(xiàn)所有頁面、所有路徑的曝光和點擊事件埋點,節(jié)省了研發(fā)人力,也不用擔(dān)心埋點邏輯會侵入正常業(yè)務(wù)邏輯。然而,不可能存在完美的解決方案,全埋點方案還是存在一些局限性:

  • 1、資源消耗較大: 全場景上報會產(chǎn)生大量無用數(shù)據(jù),網(wǎng)絡(luò)傳輸、數(shù)據(jù)存儲和數(shù)據(jù)計算需要消耗大量資源;

  • 2、頁面穩(wěn)定性要求較高: 需要保持頁面視圖結(jié)構(gòu)相對穩(wěn)定,一旦頁面視圖結(jié)果變化,歷史錄入的埋點數(shù)據(jù)就會失效;

  • 3、無法采集上下文信息: 無法采集事件產(chǎn)生時的上下文信息,也就無法滿足復(fù)雜的數(shù)據(jù)消費需求。

2.3 埋點設(shè)計的整體方案

考慮的不同方案都存在優(yōu)缺點,單純采用一種埋點方案是不切實際的,需要根據(jù)不同業(yè)務(wù)場景和不同數(shù)據(jù)消費需要而采用不同的埋點方案:

  • 1、全埋點: 作為全局兜底方案,可以滿足粗粒度的統(tǒng)計需求;

  • 2、前端埋點: 作為全埋點的補充方案,可以自定義埋點參數(shù),主要處理核心業(yè)務(wù)流程事件,例如(如登錄、注冊、收藏、購買);

  • 3、服務(wù)端埋點: 核心業(yè)務(wù)結(jié)果事件,例如訂單支付成功。


3. 前端埋點中的困難

3.1 一個簡單的埋點場景

現(xiàn)在,我們通過一個具體的埋點場景,試著發(fā)現(xiàn)在做埋點需求時會遇到的困難或痛點。我直接使用西瓜視頻中的一個埋點場景:

—— 圖片引用自西瓜視頻技術(shù)博客

這個產(chǎn)品場景很簡單,左邊是西瓜視頻的推薦流列表,點擊 “電影卡片” 會進入右邊的 “電影詳情頁” 。兩個頁面中都有 “收藏按鈕”,現(xiàn)在的數(shù)據(jù)需求是采集不同頁面中 “收藏按鈕” 的點擊事件,以便分析用戶收藏影片的行為,優(yōu)化影片的推薦模型。

  • 1、在推薦列表頁中上報點擊事件:
“event_name" : "click_favorite", // 事件名
"cur_page" : "feed",             // 當(dāng)前頁面
"video_id" : "123",              // 影片 ID
"video_name" : "影片名",          // 影片名
"video_type" : "1",              // 影片類型
"$user_id" : "10000",            // 用戶 ID
"$device_id" : "abc"             // 設(shè)備 ID
...                              // 其他預(yù)置屬性
  • 2、在電影詳情頁中上報點擊事件:
“event_name" : "click_favorite", // 事件名
"from_page" : "feed"
"cur_page" : "video_detail",     // 當(dāng)前頁面
"video_id" : "123",              // 影片 ID
"video_name" : "影片名",          // 影片名
"video_type" : "1",              // 影片類型
"$user_id" : "10000",            // 用戶 ID
"$device_id" : "abc"             // 設(shè)備 ID
...                              // 其他預(yù)置屬性

3.2 現(xiàn)狀分析

理解了這個埋點場景之后,我們先梳理出目前遇到的困難:

  • 1、埋點參數(shù)分散: 需要上報的埋點參數(shù)位于不同 UI 容器或不同業(yè)務(wù)模塊,代碼跨度很大(例如:Activity、Fragment、ViewHolder、自定義 View);

  • 2、組件復(fù)用: 組件抽象復(fù)用后在多個頁面使用(例如通用的 ViewHolder 或自定義 View);

  • 3、數(shù)據(jù)模型不一致: 不同場景 / 頁面下描述狀態(tài)的數(shù)據(jù)模型不一致,需要額外的轉(zhuǎn)換適配過程(例如有的模型用 video_type 表示影片類型,另一些模型用 videoType 表示影片類型)。

3.3 評估標(biāo)準(zhǔn)

理解了問題和現(xiàn)狀,現(xiàn)在我們開始嘗試找到解決方案。為此,我們需要想清楚理想中的解決方案,應(yīng)該滿足什么標(biāo)準(zhǔn):

  • 1、準(zhǔn)確性: 這是核心目標(biāo),能夠在保證不同場景 / 頁面下準(zhǔn)確收集埋點數(shù)據(jù);
  • 2、簡潔性: 使用方法盡可能簡單,收斂模板代碼;
  • 3、可用性: 盡可能高效穩(wěn)定,不容易出錯,性能開銷小。

3.4 常規(guī)解決方案

1、逐級傳遞 —— 通過面向?qū)ο蟮年P(guān)系逐級傳遞埋點參數(shù):

通過 Android 框架支持的 Activity / Fragment 參數(shù)傳遞方式和面向?qū)ο蟪绦蛟O(shè)計,逐級將埋點參數(shù)傳遞到最深層的收藏按鈕。例如:

  • 列表頁: Activity -> ViewModel -> FeedFragment (推薦) -> Adapter -> ViewHolder (電影卡片) -> CollectButton (收藏按鈕)

  • 詳情頁: Activity -> ViewModel -> DetailBottomFragment(底部功能區(qū)) -> CollectButton (收藏按鈕)

缺點 (參數(shù)傳遞困難) :傳遞數(shù)據(jù)需要編寫大量重復(fù)模板代碼,工程代碼膨脹,增大維護難度。再疊加上組件復(fù)用的情況,逐級傳遞會讓代碼復(fù)雜度非常高,很明顯不是一個合理的解決方案。

2、Bean 傳遞 —— 在 Java Bean 中增加字段來收集埋點參數(shù):

缺點 (違背單一職責(zé)原則):Java Bean 中侵入了與業(yè)務(wù)無關(guān)的埋點參數(shù),同時會造成 Java Bean 數(shù)據(jù)冗余,增大維護難度。

3、全局單例 —— 通過全局單例對象來收集埋點參數(shù):

這個方案與 “Bean 傳遞 ” 類似,區(qū)別在于埋點參數(shù)從 Java Bean 中移動到全局單例中,但缺點還是很明顯:

缺點 (寫入和清理時機):單例會被多個位置寫入,一旦被覆蓋就無法被恢復(fù),容易導(dǎo)致上報錯誤;另外清理的時機也難以把握,清理過早會導(dǎo)致埋點參數(shù)丟失,清理過晚會污染后面的埋點事件。


4. 西瓜視頻方案

理解了數(shù)據(jù)埋點開發(fā)中的困難,有沒有什么方案可以簡化埋點過程中的復(fù)雜度呢?我們來討論下西瓜視頻團隊分享的一個思路:基于視圖樹收集埋點參數(shù)。

—— 圖片引用自西瓜視頻技術(shù)博客

通過分析數(shù)據(jù)與視圖節(jié)點的關(guān)系可以發(fā)現(xiàn),事件的埋點數(shù)據(jù)正好分布在視圖樹的不同節(jié)點中。當(dāng) “收藏按鈕” 觸發(fā)事件時,只需要沿著視圖樹逐級向上查找 (通過 View#getParent()) 就可以收集到所有數(shù)據(jù)。

并且,樹的分支天然地支持為參數(shù)設(shè)置不同的值。例如 “推薦 Fragment” 需要上報 “channel : recomment”,而 “電影 Fragment” 需要上報 “channel : film”。因為 Fragment 的根布局對應(yīng)有視圖樹中的不同節(jié)點,所以在不同 Fragment 中觸發(fā)的事件最終收集到的 “channel” 參數(shù)值也就不同了。Nice~


5. EasyTrack 埋點框架

思路 Get 到了,現(xiàn)在我們來討論如何應(yīng)用這個思路來解決問題。貼心的我已經(jīng)幫你實現(xiàn)為一個框架 EasyTrack。源碼地址:https://github.com/pengxurui/EasyTrack

5.1 添加依賴

  • 1、依賴 JitPack 倉庫

在項目級 build.gradle 聲明遠(yuǎn)程倉庫:

allprojects {
    repositories {
        google()
        mavenCentral()
        // JitPack 倉庫
        maven { url "https://jitpack.io" }
    }
}
  • 2、依賴 EasyTrack 框架

在模塊級 build.gradle 中依賴類庫:

dependencies {
    ...
    // 依賴 EasyTrack 框架
    implementation 'com.github.pengxurui:EasyTrack:v1.0.1'
    // 依賴 Kotlin 工具(非必須)
    implementation 'com.github.pengxurui:KotlinUtil:1.0.1'
}

5.2 依附埋點參數(shù)到視圖樹

ITrackModel接口定義了一個數(shù)據(jù)填充能力,你可以創(chuàng)建它的實現(xiàn)類來定義一個數(shù)據(jù)節(jié)點,并在 fillTrackParams() 方法中聲明參數(shù)。例如:MyGoodsViewHolder 實現(xiàn)了 ITrackMode 接口,在 fillTrackParams() 方法中聲明參數(shù)(goods_id / goods_name)。

隨后,通過 View 的擴展函數(shù)View.trackModel()將其依附到視圖節(jié)點上。擴展函數(shù) View.trackModel() 內(nèi)部基于 View#setTag() 實現(xiàn)。

MyGoodsViewHolder.kt

class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ITrackModel {

    private var mItem: GoodsItem? = null

    init {
        // Java:EasyTrackUtilsKt.setTrackModel(itemView, this);
        itemView.trackModel = this
    }

     override fun fillTrackParams(params: TrackParams) {
        mItem?.let {
            params.setIfNull("goods_id", it.id)
            params.setIfNull("goods_name", it.goods_name)
        }
    }
}

EasyTrackUtils.kt

/**
 * Attach track model on the view.
 */
var View.trackModel: ITrackModel?
    get() = this.getTag(R.id.tag_id_track_model) as? ITrackModel
    set(value) {
        this.setTag(R.id.tag_id_track_model, value)
    }

ITrackModel.kt

/**
 * 定義數(shù)據(jù)填充能力
 */
interface ITrackModel : Serializable {
    /**
     * 數(shù)據(jù)填充
     */
    fun fillTrackParams(params: TrackParams)
}

5.3 觸發(fā)事件埋點

在需要埋點的地方,直接通過定義在 View 上的擴展函數(shù) trackEvent(事件名)觸發(fā)埋點事件,它會以該擴展函數(shù)的接收者對象為起點,逐級向上層視圖節(jié)點收集參數(shù)。另外,它還有多個定義在 Activity、Fragment、ViewHolder 上的擴展函數(shù),但最終都會調(diào)用到 View.trackEvent。

class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
     fun bind(item: GoodsItem) {
        ...
        trackEvent(GOODS_EXPOSE)
    }
}

EasyTrackUtils.kt

@JvmOverloads
fun Activity?.trackEvent(eventName: String, params: TrackParams? = null) =
    findRootView(this)?.doTrackEvent(eventName, params)

@JvmOverloads
fun Fragment?.trackEvent(eventName: String, params: TrackParams? = null) =
    this?.requireView()?.doTrackEvent(eventName, params)

@JvmOverloads
fun RecyclerView.ViewHolder?.trackEvent(eventName: String, params: TrackParams? = null) {
    this?.itemView?.let {
        if (null == it.parent) {
            it.post { it.doTrackEvent(eventName, params) }
        } else {
            it.doTrackEvent(eventName, params)
        }
    }
}

@JvmOverloads
fun View?.trackEvent(eventName: String, params: TrackParams? = null): TrackParams? =
    this?.doTrackEvent(eventName, params)

查看 logcat 日志,可以看到以下日志,顯示埋點并沒有生效。這是因為沒有為 EasyTrack 配置埋點數(shù)據(jù)上報和統(tǒng)計分析的能力。

logcat 日志

EasyTrackLib: Try track event goods_expose, but the providers is Empty.

5.4 實現(xiàn) ITrackProvider 接口

EasyTrack 的職責(zé)在于收集分散的埋點數(shù)據(jù),本身沒有提供埋點數(shù)據(jù)上報和統(tǒng)計分析的能力。因此,你需要實現(xiàn) ITrackProvider 接口進行依賴注入。例如,這里模擬實現(xiàn)友盟數(shù)據(jù)埋點提供器,在 onInit() 方法中進行初始化,在 onEvent() 方法中調(diào)用友盟 SDK 事件上報方法。

MockUmengProvider.kt

/**
 * 模擬友盟數(shù)據(jù)上報
 */
class MockUmengProvider : ITrackProvider() {

    companion object {
        const val TAG = "Umeng"
    }

    /**
     * 是否啟用
     */
    override var enabled = true

    /**
     * 名稱
     */
    override var name = TAG

    /**
     * 初始化
     */
    override fun onInit() {
        Log.d(TAG, "Init Umeng provider.")
    }

    /**
     * 執(zhí)行事件上報
     */
    override fun onEvent(eventName: String, params: TrackParams) {
        Log.d(TAG, params.toString())
    }
}

5.5 配置 EasyTrack

在應(yīng)用初始化時,進行 EasyTrack 的初始化配置。我們可以將相關(guān)的初始化代碼單獨封裝起來,例如:

StatisticsUtils.kt

// 模擬友盟數(shù)據(jù)統(tǒng)計提供器
val umengProvider by lazy {
    MockUmengProvider()
}

// 模擬神策數(shù)據(jù)統(tǒng)計提供器
val sensorProvider by lazy {
    MockSensorProvider()
}

/**
 * 初始化 EasyTrack,在 Application 初始化時調(diào)用
 */
fun init(context: Context) {
    configStatistics(context)
    registerProviders(context)
}

/**
 * 配置
 */
private fun configStatistics(context: Context) {
    // 調(diào)試開關(guān)
    EasyTrack.debug = BuildConfig.DEBUG
    // 頁面間參數(shù)映射
    EasyTrack.referrerKeyMap = mapOf(
        CUR_PAGE to FROM_PAGE,
        CUR_TAB to FROM_TAB
    )
}

/**
 * 注冊提供器
 */
private fun registerProviders(context: Context) {
    EasyTrack.registerProvider(umengProvider)
    EasyTrack.registerProvider(sensorProvider)
}

EventConstants.java

public static final String FROM_PAGE = "from_page";
public static final String CUR_PAGE = "cur_page";
public static final String FROM_TAB = "from_tab";
public static final String CUR_TAB = "cur_tab";
配置 類型 描述
debug Boolean 調(diào)試開關(guān)
referrerKeyMap Map<String,String> 全局頁面間參數(shù)映射
registerProvider() ITrackProvider 底層數(shù)據(jù)埋點能力

以上步驟是 EasyTrack 的必選步驟,完成后重新執(zhí)行 trackEvent() 后可以看到以下日志:

logcat 日志

/EasyTrackLib:  
onEvent:goods_expose
goods_id= 10000
goods_name = 商品名
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

5.6 頁面間參數(shù)映射

上一節(jié)中有一個referrerKeyMap配置項,定義了全局的頁面間參數(shù)映射。 舉個例子,在分析不同入口的轉(zhuǎn)化率時,不僅僅需要上報當(dāng)前頁面的數(shù)據(jù),還需要上報來源頁面的信息。這樣我們才能分析用戶經(jīng)過怎樣的路徑來到當(dāng)前頁面,并最終觸發(fā)了某個行為。

需要注意的是,來源頁面的參數(shù)往往不能直接添加到當(dāng)前頁面的埋點參數(shù)中,這里一般會有一定的轉(zhuǎn)換規(guī)則 / 映射關(guān)系。例如:來源頁面的 cur_page 參數(shù),在當(dāng)前頁面應(yīng)該映射為 from_page 參數(shù)。 在這個例子里,我們配置的映射關(guān)系是:

  • 來源頁面的 cur_page 映射為當(dāng)前頁面的 from_page;
  • 來源頁面的 cur_tab 映射為當(dāng)前頁面的 from_tab。

因此,假設(shè)來源頁面?zhèn)鬟f給當(dāng)前頁面的參數(shù)是 A,則當(dāng)前頁面在觸發(fā)事件時的收集參數(shù)是 B:

A (來源頁面):
{
    "cur_page" : "list"
    ...
}

B (當(dāng)前頁面):
{
    "cur_page" : "detail",
    "from_page" : "list",
    ...
}

BaseTrackActivity 實現(xiàn)了頁面間參數(shù)映射,你可以創(chuàng)建 BaseActivity 類并繼承于 BaseTrackActivity,或者將其內(nèi)部的邏輯遷移到你的 BaseActivity 中。這一步是可選的,如果你不使用頁面間參數(shù)映射的特性,你那大可不必使用 BaseTrackActivity。

操作 描述
定義映射關(guān)系 1、EasyTrack.referrerKeyMap 配置項
2、重寫 BaseTrackActivity #referrerKeyMap() 方法
傳遞頁面間參數(shù) Intent.referrerSnapshot(TrackParams) 擴展函數(shù)

MyGoodsDetailActivity.java

public class MyGoodsDetailActivity extends MyBaseActivity {

    private static final String EXTRA_GOODS = "extra_goods";

    public static void start(Context context, GoodsItem item, TrackParams params) {
        Intent intent = new Intent(context, GoodsDetailActivity.class);
        intent.putExtra(EXTRA_GOODS, item);
        EasyTrackUtilsKt.setReferrerSnapshot(intent, params);
        context.startActivity(intent);
    }

    @Nullable
    @Override
    protected String getCurPage() {
        return GOODS_DETAIL_NAME;
    }

    @Nullable
    @Override
    public Map<String, String> referrerKeyMap() {
        Map<String, String> map = new HashMap<>();
        map.put(STORE_ID, STORE_ID);
        map.put(STORE_NAME, STORE_NAME);
        return map;
    }
}

需要注意的是,BaseTrackActivity 不會將來源頁面的全部參數(shù)都添加到當(dāng)前頁面的參數(shù)中,只有在全局 referrerKeyMap 配置項或 referrerKeyMap() 方法中定義了映射關(guān)系的參數(shù),才會添加到當(dāng)前頁面。 例如:MyGoodsDetailActivity 繼承于 BaseActivity,并重寫 referrerKeyMap() 定義了感興趣的參數(shù)(STORE_ID、STORE_NAME)。最終觸發(fā)埋點時的日志如下:

logcat 日志

/EasyTrackLib:  
onEvent:goods_detail_expose
goods_id= 10000
goods_name = 商品名
store_id = 10000
store_name = 商店名
from_page = Recommend
cur_page = goods_detail
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

在一般的埋點模型中,每個 Activity (頁面) 都有對應(yīng)一個唯一的 page_id,因此你可以重寫 fillTrackParams() 方法追加這些固定的參數(shù)。例如:MyBaseActivity 定義了 getCurPage() 方法,子類可以通過重寫 getCurPage() 來設(shè)置 page_id。

MyBaseActivity.java

abstract class MyBaseActivity : BaseTrackActivity() {

    @CallSuper
    override fun fillTrackParams(params: TrackParams) {
        super.fillTrackParams(params)
        // 填充頁面統(tǒng)一參數(shù)
        getCurPage()?.also {
            params.setIfNull(CUR_PAGE, it)
        }
    }

    protected open fun getCurPage(): String? = null
}

5.7 TrackParams 參數(shù)容器

TrackParams 是 EasyTrack 收集參數(shù)的中間容器,最終會分發(fā)給 ITrackProvider 使用。

方法 描述
set(key: String, value: Any?) 設(shè)置參數(shù),無論無何都覆蓋
setIfNull(key: String, value: Any?) 設(shè)置參數(shù),如果已經(jīng)存在該參數(shù)則丟棄
get(key: String): String? 獲取參數(shù)值,參數(shù)不存在則返回 null
get(key: String, default: String?) 獲取參數(shù)值,參數(shù)不存在則返回默認(rèn)值 default

5.8 使用 Kotlin 委托依附參數(shù)

如果你覺得每次定義 ITrackModel 數(shù)據(jù)節(jié)點后都需要調(diào)用 View.trackModel,你可以使用我定義的 Kotlin 委托 “跳過” 這個步驟,例如:

MyFragment.kt

private val trackNode by track()

EasyTrackUtils.kt

fun <F : Fragment> F.track(): TrackNodeProperty<F> = FragmentTrackNodeProperty()

fun RecyclerView.ViewHolder.track(): TrackNodeProperty<RecyclerView.ViewHolder> =
    LazyTrackNodeProperty() viewFactory@{
        return@viewFactory itemView
    }

fun View.track(): TrackNodeProperty<View> = LazyTrackNodeProperty() viewFactory@{
    return@viewFactory it
}

如果你還不了解委托屬性,可以看下我之前寫過的一篇文章,這里不解釋其原理了:Android | ViewBinding 與 Kotlin 委托雙劍合璧


6. EasyTrack 核心源碼

這一節(jié),我簡單介紹下 EasyTrack 的核心源碼,最核心的部分在入口類 EasyTrack 中:

6.1 doTrackEvent()

doTrackEvent() 是觸發(fā)埋點的主方法,主要流程是調(diào)用 fillTrackParams() 收集埋點參數(shù),再將參數(shù)分發(fā)給有效的 ITrackProvider。

internal fun Any.doTrackEvent(eventName: String, otherParams: TrackParams? = null): TrackParams? {
    1. 檢查是否有有效的 ITrackProvider
    2. 基于視圖樹遞歸收集埋點參數(shù)(fillTrackParams)
    3. 日志
    4. 將收集到的埋點參數(shù)分發(fā)給有效的 ITrackProvider
}

6.2 fillTrackParams()

-> 基于視圖樹遞歸收集埋點參數(shù)
internal fun fillTrackParams(node: Any?, params: TrackParams? = null): TrackParams {
    val result = params ?: TrackParams()
    var curNode = node
    while (null != curNode) {
        when (curNode) {
            is View -> {
                // 1. 視圖節(jié)點
                if (android.R.id.content == curNode.id) {
                    // 1.1 Activity 節(jié)點
                    val activity = getActivityFromView(curNode)
                    if (activity is IPageTrackNode) {
                        // 1.1.1 IPageTrackNode節(jié)點(處理頁面間參數(shù)映射)
                        activity.fillTrackParams(result)
                        curNode = activity.referrerSnapshot()
                    } else {
                        // 1.1.2 終止
                        curNode = null
                    }
                } else {
                    // 1.2 Activity 視圖子節(jié)點
                    curNode.trackModel?.fillTrackParams(result)
                    curNode = curNode.parent
                }
            }
            is ITrackNode -> {
                // 2. 非視圖節(jié)點
                curNode.fillTrackParams(result)
                curNode = curNode.parent
            }
            else -> {
                // 3. 終止
                curNode = null
            }
        }
    }
    return result
}

主要邏輯:從入?yún)?node 為起點,循環(huán)獲取依附在視圖節(jié)點上的 ITrackModel 數(shù)據(jù)節(jié)點并調(diào)用 fillTrackParams() 方法收集參數(shù),并將循環(huán)指針指向 parent。


7. 總結(jié)

EasyTrack 框架的源碼我已經(jīng)放在 Github 上了,源碼地址:https://github.com/pengxurui/EasyTrack 我也寫了一個簡單的 Sample Demo,你可以直接運行體驗下。歡迎批評,歡迎 Issue~

說說目前遇到的問題,在處理頁面間參數(shù)傳遞時,我們需要依賴 Intent extras 參數(shù)。這就導(dǎo)致我們需要在大量創(chuàng)建 Intent 的地方都加入來源頁面的埋點參數(shù)(注意:即使你不使用 EasyTrack,你也要這么做)。目前我還沒有想到比較好的方法,你覺得呢?說說你的看法吧。


參考資料

創(chuàng)作不易,你的「三連」是丑丑最大的動力,我們下次見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容