
點贊關(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,你也要這么做)。目前我還沒有想到比較好的方法,你覺得呢?說說你的看法吧。
參考資料
- 西瓜客戶端埋點實踐:基于責(zé)任鏈的埋點框架 —— 何金海(字節(jié))著
- 埋點治理:如何把 App 埋點做到極致? —— 林樂洋(58)著
- 51 信用卡 Android 自動埋點實踐 —— 李傳志(51 信用卡)著
- 利用 Live Templates 打造埋點自動化利器 —— 字節(jié)大力智能技術(shù)團隊 著
- 數(shù)據(jù)分析從理念到實操 —— 神策數(shù)據(jù) 著
創(chuàng)作不易,你的「三連」是丑丑最大的動力,我們下次見!
