一個(gè)好的產(chǎn)品離不開數(shù)據(jù)分析,在手機(jī) APP 中,數(shù)據(jù)分析極致化需要細(xì)致到某個(gè)時(shí)刻列表曝光的了哪幾個(gè) Item。
2022 年了,基本上目前 Android 上可以滑動(dòng)的復(fù)雜列表都是 RecyclerView 或者其擴(kuò)展,這里分享一個(gè)封裝的思路。
一、基本思路
什么是列表曝光
簡(jiǎn)單的理解就是用戶在肉眼可感知范圍內(nèi)真正看到了數(shù)據(jù)就算曝光,包括數(shù)據(jù)刷新了
如果非要細(xì)化細(xì)節(jié):
- 1、列表數(shù)據(jù)變化時(shí),比如上滑下滑
- 2、頁(yè)面從隱藏到顯示,比如切換頁(yè)面、前后臺(tái)切換
一些方案的對(duì)比
各種方案核心都差不多,最關(guān)鍵的就是通過 LayoutManager 獲取屏幕內(nèi)第一個(gè)可見和最后一個(gè)可見 item position,上報(bào)其區(qū)間內(nèi)的 Item。這里簡(jiǎn)稱這個(gè)邏輯為檢查上報(bào)邏輯。
但是觸發(fā)時(shí)機(jī)有所不同,通常如下方案一和二所述,當(dāng)然除了方案一和方案二外,還有一些別的方案,比如監(jiān)聽 RecyclerView 的布局樹變化觸發(fā)檢查上報(bào)邏輯等方案。
方案一
- 1、監(jiān)聽列表數(shù)據(jù)變化,比如 RecyclerView 通過監(jiān)聽 Adapter 的數(shù)據(jù)變化,數(shù)據(jù)變化之后觸發(fā)檢查上報(bào)邏輯
- 2、監(jiān)聽列表滑動(dòng),在列表停止滑動(dòng)時(shí)觸發(fā)檢查上報(bào)邏輯
- 3、頁(yè)面隱藏到顯示的時(shí)候觸發(fā)檢查上報(bào)邏輯
方案二
這個(gè)是在想降低曝光埋點(diǎn)復(fù)雜度時(shí),閱讀 RecyclerView 源碼,并且經(jīng)過 Demo 不斷測(cè)試和調(diào)試發(fā)現(xiàn)的新路子 ??
- 1、通過注冊(cè)
RecycleView的OnChildAttachStateChangeListener接口來監(jiān)聽子 view attached 和 detached 的情況,這個(gè)接口有個(gè)特點(diǎn):子 View 滑動(dòng)到可以RecyclerView區(qū)域內(nèi)時(shí)會(huì)觸發(fā)onChildViewAttachedToWindow,相反移出RecyclerView區(qū)域外則觸發(fā)onChildViewDetachedFromWindow,正所謂天然的觸發(fā)曝光的接口,我們可以建立收集數(shù)據(jù)集邏輯, 在onChildViewAttachedToWindow時(shí)加入 item 到集合,onChildViewDetachedFromWindow時(shí)從集合移除 item,在人眼可以感知到的時(shí)間內(nèi)比如收集行為結(jié)束 500ms 后統(tǒng)一匯總集合中的 item,將 item 一一上報(bào)。 - 2、頁(yè)面隱藏到顯示的時(shí)候觸發(fā)檢查上報(bào)邏輯
可以發(fā)現(xiàn)方案二相比方案一更有利于減少各種回調(diào)的注冊(cè)和周期的控制,下文會(huì)在方案二的基礎(chǔ)上,闡述用法和相關(guān)實(shí)現(xiàn)思路。

二、RecyclerViewExposure 庫(kù)用法
倉(cāng)庫(kù)地址:RecyclerViewExposure
優(yōu)點(diǎn):
- 1、抽象相關(guān)統(tǒng)計(jì)埋點(diǎn)和生命周期管理
- 2、支持
ConcatAdapter(MergeAdapter)
缺點(diǎn):
- 1、未支持 Item 可見程度百分比觸發(fā)曝光邏輯(由于相對(duì)耗費(fèi)計(jì)算性能,在曝光埋點(diǎn)場(chǎng)景暫不允支持,不過留了擴(kuò)展的方法)
- 2、僅僅支持流式列表和網(wǎng)格列表(網(wǎng)格也是流式列表的一種)(當(dāng)然可以通過修改核心檢查上報(bào)邏輯達(dá)到支持流式和其他列表的目的)
業(yè)務(wù)場(chǎng)景:
- 1、有一個(gè) size 為 n 的列表
- 2、當(dāng)列表曝光時(shí),在用戶可感知范圍內(nèi)上報(bào)用戶能看到的 item 的信息
- 可感知:快速滑動(dòng)時(shí),只有最后停下來看到的 item才算是可感知,慢速移動(dòng)時(shí),能肉眼看到的 item 都算是可感知
配置 Gradle 依賴
-
在 project 級(jí)別的 build.gradle 中
buildscript { repositories { ... //booster maven { url 'https://oss.sonatype.org/content/repositories/public/' } //exposure_plugin maven { url 'https://jitpack.io' } } dependencies { classpath "com.didiglobal.booster:booster-gradle-plugin:4.5.3" //插件 classpath "com.github.minminaya.RecyclerViewExposure:exposure_plugin:0.0.3" } } allprojects { repositories { ... maven { url 'https://jitpack.io' } ... } } -
在 app 級(jí)別的 build.gradle 中
plugins { ... //應(yīng)用插件,也可以使用 apply plugin: 'com.didiglobal.booster' 的寫法 id 'com.didiglobal.booster' } dependencies { //依賴 implementation 'com.github.minminaya.RecyclerViewExposure:exposure:0.0.3' }
API 說明
-
IEntityForImpr:接口,需要上報(bào)的列表 Adapter 的數(shù)據(jù)實(shí)體實(shí)現(xiàn)該接口,并實(shí)現(xiàn)方法getIdForImpr(),目的是為了讓 item 保持唯一性 -
AbsListImprEventHelper:抽象類,實(shí)現(xiàn)曝光事件上報(bào)的封裝類,針對(duì)列表數(shù)據(jù)Adapter 為RecyclerView.Adapter<K>的子類做了封裝- ①
needPostEvent():該方法表示當(dāng)前的item是否需要上報(bào)統(tǒng)計(jì),返回false表示不需要上報(bào)該item - ②
getAdapterEntityForPosition():該方法返回item對(duì)應(yīng)的entity - ③
onItemExposure():?jiǎn)蝹€(gè) Item 曝光的時(shí)候回調(diào),回調(diào)數(shù)據(jù)為entity absoluteAdapterPosition bindingAdapterPosition - ④
onBatchItemExposure():可見項(xiàng)批量曝光回調(diào),一次回調(diào)出所有 item 相關(guān)的數(shù)據(jù)類Triple,包含entity absoluteAdapterPosition bindingAdapterPosition數(shù)據(jù)
- ①
-
AbsListAdapterImprEventHelper:抽象類,擴(kuò)展自AbsListImprEventHelper類,針對(duì)列表數(shù)據(jù)Adapter 為ListAdapter的子類做了封裝,定義實(shí)現(xiàn)了getAdapterEntityForPosition()的方法,讓RecyclerViewExposure的使用方法更加精簡(jiǎn)
使用方法
首先我們先實(shí)現(xiàn)一個(gè)列表(部分實(shí)現(xiàn)省略)
-
1、創(chuàng)建列表適配器
ItemRecyclerViewAdapterclass ItemRecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { val dataList = mutableListOf<PlaceholderContent.PlaceholderItem>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( FragmentItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } inner class ViewHolder(binding: FragmentItemBinding) : RecyclerView.ViewHolder(binding.root) { val idView: TextView = binding.itemNumber val contentView: TextView = binding.content override fun toString(): String { return super.toString() + " '" + contentView.text + "'" } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = dataList[position] (holder as? ViewHolder)?.apply { idView.text = item.id contentView.text = item.content } } override fun getItemCount(): Int { return dataList.size } } -
2、創(chuàng)建
ItemRecyclerViewAdapter需要用的數(shù)據(jù)實(shí)體類PlaceholderContent.PlaceholderItemdata class PlaceholderItem(val id: String, val content: String, val details: String) -
3、創(chuàng)建 Activity 容器,綁定 xml 布局
class RecyclerAdapterExampleActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_example) val recyclerview = findViewById<RecyclerView>(R.id.list) recyclerview.layoutManager = LinearLayoutManager(this) val adapter = ItemRecyclerViewAdapter() recyclerview.adapter = adapter adapter.dataList.addAll(PlaceholderContent.ITEMS) adapter.notifyDataSetChanged() } }<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.minminaya.example.activity.ListAdapterExampleActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/list" android:name="com.minminaya.example.ItemFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" app:layoutManager="LinearLayoutManager" tools:context="com.minminaya.example.activity.ListAdapterExampleActivity" tools:listitem="@layout/fragment_item" /> </androidx.constraintlayout.widget.ConstraintLayout> -
4、列表如下
接入 RecyclerViewExposure 庫(kù)
-
1、讓 PlaceholderItem 類實(shí)現(xiàn)
IEntityForImpr接口,實(shí)現(xiàn)getIdForImpr()方法,返回 item 的唯一標(biāo)志data class PlaceholderItem(val id: String, val content: String, val details: String) : IEntityForImpr { override fun toString(): String = content override fun getIdForImpr(): String { return id } } -
2、新建埋點(diǎn)幫助類
RecyclerViewAdapterImprEventHelper,讓其繼承自AbsListImprEventHelperclass RecyclerViewAdapterImprEventHelper( recyclerView: RecyclerView, componentActivity: ComponentActivity ) : AbsListImprEventHelper<ItemRecyclerViewAdapter, PlaceholderContent.PlaceholderItem>( recyclerView, componentActivity ) { /** * 是否需要統(tǒng)計(jì)曝光事件 * * @param entity entity */ override fun needPostEvent(entity: PlaceholderContent.PlaceholderItem): Boolean { return true } /** * 當(dāng)bindingAdapterPosition項(xiàng)曝光的時(shí)候回調(diào) * * @param entity entity * @param absoluteAdapterPosition 相對(duì) RecyclerView 的 item position * @param bindingAdapterPosition 相對(duì)子 Adapter 級(jí)別 item position */ override fun onItemExposure( entity: PlaceholderContent.PlaceholderItem, absoluteAdapterPosition: Int, bindingAdapterPosition: Int ) { //上報(bào)邏輯,通常是調(diào)用某些統(tǒng)計(jì) sdk Log.d( "RecyclerViewAdapterImprEventHelper", "onItemExposure:---- absoluteAdapterPosition:$absoluteAdapterPosition ,$entity" ) } /** * 抽象提供 Adapter 中數(shù)據(jù)集合對(duì)象 * * @param bindingAdapterPosition sub Adapter中的位置 * @param viewHolder viewHolder */ override fun getAdapterEntityForPosition( bindingAdapterPosition: Int, viewHolder: RecyclerView.ViewHolder ): PlaceholderContent.PlaceholderItem? { //自定義返回 Adapter 中某個(gè) item 對(duì)應(yīng)的數(shù)據(jù) return (viewHolder.bindingAdapter as? ItemRecyclerViewAdapter)?.let { if (bindingAdapterPosition in 0 until it.dataList.size) { return@let it.dataList[bindingAdapterPosition] } else null } } /** * 可見項(xiàng)批量曝光回調(diào) * * @param tripleList 包含entity absoluteAdapterPosition bindingAdapterPosition的數(shù)據(jù)類 * @apiNote entity entity * @apiNote absoluteAdapterPosition 相對(duì) RecyclerView 的 item position * @apiNote bindingAdapterPosition 相對(duì)子 Adapter級(jí)別 item position */ override fun onBatchItemExposure(tripleList: MutableList<Triple<PlaceholderContent.PlaceholderItem, Int, Int>>) { super.onBatchItemExposure(tripleList) Log.d( "Event", "onBatchItemExposure:---- tripleList:$tripleList" ) } }-
將 Adapter 的類聲明和 Adapter Item 的數(shù)據(jù)類聲明作為范型補(bǔ)充到
RecyclerViewAdapterImprEventHelper中class RecyclerViewAdapterImprEventHelper( recyclerView: RecyclerView, componentActivity: ComponentActivity ) : AbsListImprEventHelper<ItemRecyclerViewAdapter, PlaceholderContent.PlaceholderItem>( recyclerView, componentActivity ) { ...AbsListImprEventHelper 內(nèi)部會(huì)判斷某個(gè)曝光的 Item 是否屬于指定的 Adapter 從而做前置數(shù)據(jù)過濾,非指定 Adapter 的數(shù)據(jù)曝光會(huì)被丟棄
-
實(shí)現(xiàn)
getAdapterEntityForPosition()override fun getAdapterEntityForPosition( bindingAdapterPosition: Int, viewHolder: RecyclerView.ViewHolder ): PlaceholderContent.PlaceholderItem? { //自定義返回 Adapter 中某個(gè) item 對(duì)應(yīng)的數(shù)據(jù) return (viewHolder.bindingAdapter as? ItemRecyclerViewAdapter)?.let { if (bindingAdapterPosition in 0 until it.dataList.size) { return@let it.dataList[bindingAdapterPosition] } else null } }這里主要目的是為了獲取當(dāng)前某個(gè)position 對(duì)應(yīng)的 Item 數(shù)據(jù),這里我簡(jiǎn)單通過 bindingAdapterPosition 和 Adapter 中的 ItemList 獲取特定的 item 值
-
-
3、在 Activity 中應(yīng)用
RecyclerViewAdapterImprEventHelper類override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_example) val recyclerview = findViewById<RecyclerView>(R.id.list) recyclerview.layoutManager = LinearLayoutManager(this) val adapter = ItemRecyclerViewAdapter() recyclerview.adapter = adapter adapter.dataList.addAll(PlaceholderContent.ITEMS) adapter.notifyDataSetChanged() RecyclerViewAdapterImprEventHelper(recyclerview, this) } 只需要對(duì)
RecyclerViewAdapterImprEventHelper進(jìn)行實(shí)例化即可,無需手動(dòng)維護(hù)某些組件的生命周期,框架內(nèi)部自動(dòng)維護(hù)-
4、這個(gè)例子中,當(dāng)列表曝光時(shí),將輸出日志
2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:0 ,Item 1 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:1 ,Item 2 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:2 ,Item 3 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:3 ,Item 4 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:4 ,Item 5 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:5 ,Item 6 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:6 ,Item 7 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:7 ,Item 8 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:8 ,Item 9 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:9 ,Item 10 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:10 ,Item 11 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:11 ,Item 12 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:12 ,Item 13 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onItemExposure:---- absoluteAdapterPosition:13 ,Item 14 2022-03-19 22:49:22.699 13872-13910/com.minminaya.example D/Event: onBatchItemExposure:---- tripleList:[(Item 1, 0, 0), (Item 2, 1, 1), (Item 3, 2, 2), (Item 4, 3, 3), (Item 5, 4, 4), (Item 6, 5, 5), (Item 7, 6, 6), (Item 8, 7, 7), (Item 9, 8, 8), (Item 10, 9, 9), (Item 11, 10, 10), (Item 12, 11, 11), (Item 13, 12, 12), (Item 14, 13, 13)]列表曝光將按照單個(gè)用
onItemExposure回調(diào),單個(gè)回調(diào)結(jié)束后將會(huì)調(diào)用批量曝光方法onBatchItemExposure
其他優(yōu)化
- 數(shù)據(jù)列表 Adapter 繼承自 ListAdapter:
RecyclerViewExposure 內(nèi)部補(bǔ)充了關(guān)于ListAdapter的getAdapterEntityForPosition()的方法實(shí)現(xiàn),對(duì)于ListAdapter的列表曝光,我們可以直接繼承AbsListAdapterImprEventHelper,補(bǔ)充needPostEvent()和onItemExposure方法的聲明即可。class ListAdapterImprEventHelper( recyclerView: RecyclerView, fragment: Fragment ) : AbsListAdapterImprEventHelper<ItemRecyclerViewListAdapter, PlaceholderContent.PlaceholderItem>( recyclerView, fragment ) { override fun needPostEvent(entity: PlaceholderContent.PlaceholderItem): Boolean { return true } override fun onItemExposure( entity: PlaceholderContent.PlaceholderItem, absoluteAdapterPosition: Int, bindingAdapterPosition: Int ) { Log.d( "ListAdapterImprEventHelper", "onItemExposure:---- absoluteAdapterPosition:$absoluteAdapterPosition ,$entity" ) } }
三、源碼實(shí)現(xiàn)
源碼目錄

-
/container:存放了狀態(tài)分發(fā)需要使用的 Activity/Fragment 容器類 -
/pagestate:存放了狀態(tài)分發(fā)需要用到的相關(guān)接口和狀態(tài)枚舉 -
AbsListImprEventHelper:RecyclerViewExposure庫(kù)的主要邏輯實(shí)現(xiàn)類,承擔(dān)埋點(diǎn)的收集曝光和曝光分發(fā)邏輯 -
AbsListAdapterImprEventHelper:擴(kuò)展自 AbsListAdapterImprEventHelper,針對(duì) ListAdapter 類型列表封裝的 EventHelper 類
源碼分析
這里會(huì)主要說明一些主要邏輯,需要完整的邏輯可以 fork 倉(cāng)庫(kù) 查看
思路說明
- 1、為 RecyclerViewExposure 庫(kù)提供頁(yè)面可見非可見狀態(tài)監(jiān)聽
頁(yè)面通常分為 Activity 和 Fragment- Activity:只需要監(jiān)聽 onStart 和 onStop 即可(比如使用 LifeCycle 就可以簡(jiǎn)單的做到)
- Fragment:由于 Fragment 有 hide 這種使用方式,F(xiàn)ragment 的聲明周期涉及比較復(fù)雜,我們通過
onHiddenChangedonResumeonPause一起結(jié)合判斷當(dāng)前 Fragment 是否是可見狀態(tài) - 這里會(huì)模仿 Lifecycle 的狀態(tài)分發(fā)方式,新建專門用于頁(yè)面可見性的生命周期
PageLifeCycleHolder類,用來維護(hù)PageState.VISIBLE和PageState.INVISIBLE狀態(tài),分別表示頁(yè)面當(dāng)前為可見和非可見狀態(tài)。 - 缺點(diǎn):模仿
Lifecycle的實(shí)現(xiàn),它通過在ComponentActivity/Fragment中維護(hù)和分發(fā)各個(gè)生命周期, 來監(jiān)聽Lifecycle.State的變化,并且將數(shù)據(jù)變化分發(fā)給外部注冊(cè)者,這種方式需要將分發(fā)代碼耦合在項(xiàng)目基類Activity 和 Fragment 中,不優(yōu)雅不易轉(zhuǎn)移使用,接入成本稍微高。 - 那么怎么解決呢???
如果非要使用這種頁(yè)面狀態(tài)分發(fā)的形式,而且還不能改變項(xiàng)目原 Activity/Fragment基類的繼承方式,不能編碼級(jí)別的改變,那我們可以編譯的時(shí)候給加上PageLifeCycleHolder的狀態(tài)分發(fā)代碼,這種方式比較常見的做法就是在 Gradle 編譯流程中,編寫 Gradle 插件,自定義 Transform ,Transform 中使用 ASM/Javassist 來修改最終的class來達(dá)到類似 AOP 的目的。 - 這里為了編譯性能更好,選擇 ASM 來進(jìn)行代碼的修改,當(dāng)然 Javassist 也可以,甚至因?yàn)?Javassist API 抽象程度相當(dāng)高,導(dǎo)致其編寫成本更低。ASM 需要開發(fā)者熟悉 Class 文件體系、JVM 指令集,ASM API 的使用。不過為了 RecyclerViewExposure 不對(duì)宿主項(xiàng)目編譯速度造成較大影響,選擇使用編譯速度更快的 ASM。
- 2、RecyclerView 的Item 可見項(xiàng)和非可見管理和收集
- 如何滿足收集條件
- 在 RecyclerView 中,結(jié)合
OnChildAttachStateChangeListener接口,這個(gè)比較容易做到,當(dāng)OnChildAttachStateChangeListener接口回調(diào)onChildViewAttachedToWindow()時(shí),記錄 attached 的 item 到全局集合中,當(dāng)回調(diào)onChildViewDetachedFromWindow()時(shí)將item 從全局集合中去掉,等待 attached/dettached 行為結(jié)束 600ms 后(可調(diào)整,這里視為 item 被看到 600ms 才算是曝光),對(duì)集合中的剩余 item 觸發(fā)上報(bào)曝光的邏輯
- 在 RecyclerView 中,結(jié)合
- 可管理收集 Item 條件?(item 可見百分比等)
- 因?yàn)槲覀兪占饺至斜碇?,我們可以?viewholder 中拿到 view,我們可以通過判斷列表可見的第一個(gè) item 和列表可見的最后一個(gè) item 的 View坐標(biāo)范圍和 RecyclerView 自身的 View 坐標(biāo)范圍計(jì)算,判斷第一個(gè)和最后一個(gè) item 的可見區(qū)間是否滿足可見大于某個(gè)百分比【這個(gè)特性RecyclerViewExposure 沒有支持,思路共參考】
- 如何滿足收集條件
- 3、使用方法優(yōu)化精簡(jiǎn)
- 考慮到應(yīng)用程要開放些什么信息
- 通過范型減少類抽象方法
源碼設(shè)計(jì)
1、為 RecyclerViewExposure 庫(kù)提供頁(yè)面可見非可見狀態(tài)監(jiān)聽
思路來自于 lifecycle 的設(shè)計(jì),這里主要是想讓 Activity/Fragment 提供可見和不可見的狀態(tài)變化給外部訂閱
-
可見,不可見狀態(tài)定義到 PageState 枚舉中
enum class PageState(val number: Int) { /** * 可見狀態(tài) */ VISIBLE(2), /** * 不可見狀態(tài) */ INVISIBLE(3), } -
定義 PageLifeCyclerHolder,它的職責(zé)是分發(fā)管理 PageState 的狀態(tài)
class PageLifeCycleHolder(private val lifecycle: Lifecycle) : LifecycleObserver, IPageStateObserver { var pageState: PageState = PageState.INVISIBLE private val pageLifeCycleObserverList by lazy { return@lazy mutableListOf<IPageLifeCycleObserver>() } /** * 內(nèi)部會(huì)自動(dòng)解綁 * * @param observer IPageLifeCycleObserver */ @MainThread fun addPageObserver(observer: IPageLifeCycleObserver) { if (pageLifeCycleObserverList.contains(observer)) { return } pageLifeCycleObserverList.add(observer) } @MainThread fun removePageObserver(observer: IPageLifeCycleObserver) { pageLifeCycleObserverList.remove(observer) } private fun onDestroy() { pageLifeCycleObserverList.forEach { it.onDestroy() } lifecycle.removeObserver(this) pageLifeCycleObserverList.clear() } override fun onPageState(pageState: PageState) { if (this.pageState == pageState) { //避免相同狀態(tài)回調(diào)多次 return } this.pageState = pageState pageLifeCycleObserverList.forEach { it.onPageState(pageState) when (pageState) { PageState.VISIBLE -> { it.onPageVisible() } PageState.INVISIBLE -> { it.onPageInvisible() } } } } init { lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { super.onDestroy(owner) onDestroy() } }) } }主要實(shí)現(xiàn)在
onPageState(pageState: PageState),供外部頁(yè)面容器Activity/Fragment控制頁(yè)面的可見和不可見狀態(tài),當(dāng)狀態(tài)發(fā)生變化,那么將狀態(tài)分發(fā)給訂閱了狀態(tài)變化的各處地方。 -
在項(xiàng)目的 Activity 基類中補(bǔ)充 PageLifeCycleHolder 全局變量和通過 onStart 和 onStop 分發(fā)狀態(tài)
public class BaseActivity extends AppCompatActivity implements IPageStateLifecycleOwner { private PageLifeCycleHolder mPageLifeCycleHolder; @NonNull @Override public PageLifeCycleHolder getPageStateLifecycle() { if (mPageLifeCycleHolder == null) { initPageLifeCycleHolder(); } return mPageLifeCycleHolder; } @Override public void initPageLifeCycleHolder() { mPageLifeCycleHolder = new PageLifeCycleHolder(getLifecycle()); } @CallSuper @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); initPageLifeCycleHolder(); } @Override protected void onResume() { super.onResume(); getPageStateLifecycle().onPageState(PageState.VISIBLE); } @Override protected void onStop() { super.onStop(); getPageStateLifecycle().onPageState(PageState.INVISIBLE); } }那有的同學(xué)就問了,你這????我用個(gè)庫(kù)還得改 Activity 基類???另外上面目錄中的 WrapExposureActivity 是干什么用的呢?
確實(shí),用個(gè)庫(kù)還改基類確實(shí)挺爽(keng)的,為此我打算使用 ASM 編譯插入代碼的方式來補(bǔ)充上述基類的代碼到 ComponentActivity 中
插入代碼有兩種方式: 1、通過修改
ComponentActivity的onResume方法和onStop方法的實(shí)現(xiàn)已經(jīng)讓ComponentActivity實(shí)現(xiàn)IPageStateLifecycleOwner接口達(dá)到目的2、通過新建一個(gè)
WrapExposureActivity繼承自ComponentActivity類,將上述基類中的代碼補(bǔ)充到此,編譯過程將繼承了ComponentActivity的類,全部修改為WrapExposureActivity,相當(dāng)于強(qiáng)行在繼承關(guān)系中插了一腿子 ??3、考慮到
Fragment也是類似的方案進(jìn)行基類代碼補(bǔ)充,Fragment相關(guān)的邏輯還是稍微有點(diǎn)復(fù)雜的,全部改為 ASM 的方式實(shí)現(xiàn)會(huì)比較麻煩,而且為了防止后續(xù) Android 版本的Activity/FragmentAPI更新導(dǎo)致 ASM 插樁失敗,這里選用方案 2,將基類代碼寫好在一個(gè)類中,通過 ASM 修改繼承關(guān)系最終達(dá)到基類擁有PageLifeCycleHolder的目的。-
新建 WrapComponentActivity,主要是上述 BaseActivity 的代碼,用于后續(xù)給Gradle ASM 插樁修改 Activity 的繼承關(guān)系使用
public class WrapExposureActivity extends ComponentActivity implements IPageStateLifecycleOwner { ... 省略相關(guān)實(shí)現(xiàn),見上面的 BaseActivity } -
新建 WrapExposureFragment,其作用類似 WrapComponentActivity,用于后續(xù)給Gradle ASM 插樁修改 Fragment的繼承關(guān)系使用
public class WrapExposureFragment extends Fragment implements IPageStateLifecycleOwner { /** * 曾經(jīng)有顯示過界面 */ protected boolean hasResume = false; private PageLifeCycleHolder mPageLifeCycleHolder; @Override public void onResume() { super.onResume(); if (!isHidden()) { onFragmentVisible(true); } hasResume = true; } @Override public void onPause() { super.onPause(); if (!isHidden()) { onFragmentVisible(false); } hasResume = false; } @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); if (hasResume) { onFragmentVisible(!hidden); } } /** * @param isVisible true 代表顯示 */ @CallSuper protected void onFragmentVisible(boolean isVisible) { if (isVisible) { getPageStateLifecycle().onPageState(PageState.VISIBLE); } else { getPageStateLifecycle().onPageState(PageState.INVISIBLE); } } @NonNull @Override public PageLifeCycleHolder getPageStateLifecycle() { if (mPageLifeCycleHolder == null) { initPageLifeCycleHolder(); } return mPageLifeCycleHolder; } @Override public void initPageLifeCycleHolder() { mPageLifeCycleHolder = new PageLifeCycleHolder(getLifecycle()); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); initPageLifeCycleHolder(); } }Fragment的可見和不可見狀態(tài)分發(fā)依靠onHiddenChanged和onResumeonPause的結(jié)合達(dá)到目的,最終通過onFragmentVisible來對(duì)PageLifeCycleHolder分發(fā)狀態(tài) -
新建 Gradle 插件,通過ASM 修改繼承關(guān)系
@AutoService(ClassTransformer::class) class PageLifeCycleHolderTransformer : ClassTransformer { override fun transform(context: TransformContext, klass: ClassNode): ClassNode { //忽略 WrapExposureFragment 和 WrapExposureActivity if (klass.name in IGNORE_CLASS_NAME_LIST) { return klass } //將繼承自 androidx/activity/ComponentActivity 的類的父類改為 IGNORE_ACTIVITY_NAME if (klass.superName == "androidx/activity/ComponentActivity") { klass.superName = IGNORE_ACTIVITY_NAME } //將繼承自 androidx/fragment/app/Fragment 的類的父類改為 IGNORE_FRAGMENT_NAME if (klass.superName == "androidx/fragment/app/Fragment") { klass.superName = IGNORE_FRAGMENT_NAME } return klass } companion object { private const val IGNORE_FRAGMENT_NAME = "com/minminaya/exposure/container/WrapExposureFragment" private const val IGNORE_ACTIVITY_NAME = "com/minminaya/exposure/container/WrapExposureActivity" private val IGNORE_CLASS_NAME_LIST = listOf( IGNORE_ACTIVITY_NAME, IGNORE_FRAGMENT_NAME, ) } }主要就是將繼承自
ComponentActivity和Fragment的類的繼承關(guān)系改為WrapExposureActivity/WrapExposureFragment,相比直接對(duì)ComponentActivity和Fragment直接插入代碼簡(jiǎn)單和穩(wěn)定。
2、RecyclerView 的Item 可見項(xiàng)和非可見管理和收集
對(duì) List Item 的收集處理是 RecyclerViewExposure 最核心的收集數(shù)據(jù)邏輯,這里針對(duì)在 Activity 的使用作為例子。上文已經(jīng)講述如何做一個(gè) PageLifeCycleHolder 為其他組件提供頁(yè)面可見狀態(tài),下文將直接使用。
-
1、新建曝光埋點(diǎn)幫助類 AbsListImprEventHelper,傳入兩個(gè)范型,L 代表當(dāng)前使用的列表的實(shí)際 Adapter,T 代表當(dāng)前列表使用的數(shù)據(jù)
public abstract class AbsListImprEventHelper<L extends RecyclerView.Adapter<?>, T extends IEntityForImpr> implements IListImpEventHelper, IPageLifeCycleObserver, RecyclerView.OnChildAttachStateChangeListener { }RecyclerViewExposure 在收集數(shù)據(jù)的過程中會(huì)使用范型 L 來過濾RecyclerView 中 L 類型 Adapter的子項(xiàng)數(shù)據(jù)
/** * @return 提供待統(tǒng)計(jì)的目標(biāo)Sub Adapter class類型 */ @SuppressWarnings("unchecked") @NotNull public Class<?> getRecyclerViewSubAdapterClazz() { if (mRecyclerViewAdapterClass != null) { return mRecyclerViewAdapterClass; } Type type = getClass().getGenericSuperclass(); try { Type[] parameter = ((ParameterizedType) type).getActualTypeArguments(); mRecyclerViewAdapterClass = (Class<L>) parameter[0]; return mRecyclerViewAdapterClass; } catch (Exception exception) { exception.printStackTrace(); return Object.class; } } private boolean isBindingAdapter(RecyclerView.ViewHolder viewHolder) { if (viewHolder == null) { return false; } return viewHolder.getBindingAdapter() != null && viewHolder.getBindingAdapter().getClass() == getRecyclerViewSubAdapterClazz(); }通過獲取 Class 的第一個(gè)范型類型拿到 L 對(duì)應(yīng)的 Class 對(duì)象,收集數(shù)據(jù)過程中,通過判斷
isBindingAdapter()來過濾出對(duì)應(yīng)Adapter的數(shù)據(jù),這也是RecyclerViewExposure庫(kù)兼容ConcatAdapter(MergeAdapter)的原因 -
3、構(gòu)造方法初始化相關(guān)監(jiān)聽器
protected AbsListImprEventHelper(@NonNull RecyclerView recyclerView, @NonNull ComponentActivity componentActivity) { this.mRecyclerView = recyclerView; PageLifeCycleHolder pageLifeCycleHolder; if (componentActivity instanceof IPageStateLifecycleOwner) { IPageStateLifecycleOwner pageStateLifecycleOwner = (IPageStateLifecycleOwner) componentActivity; pageLifeCycleHolder = pageStateLifecycleOwner.getPageStateLifecycle(); } else { throw new RuntimeException( "please add below classpath to build.gradle at project root.\n" + "\"com.didiglobal.booster:booster-gradle-plugin:{booster-gradle-plugin-version}\"\n" + ",\"com.minminaya:exposure-plugin:{exposure-plugin-version}\""); } init(pageLifeCycleHolder); } private void init(@NonNull PageLifeCycleHolder pageLifeCycleHolder) { pageLifeCycleHolder.addPageObserver(this); if (pageLifeCycleHolder.getPageState() == PageState.VISIBLE) { onPageStart(); checkAndPostEvent(mRecyclerView); } } private void onPageStart() { if (mRecyclerView != null && !isAddOnChildAttachStateChangeListener) { isAddOnChildAttachStateChangeListener = true; mRecyclerView.addOnChildAttachStateChangeListener(this); } }構(gòu)造方法主要選擇在列表可見的時(shí)候初始化
OnChildAttachStateChangeListener接口和初始化時(shí)進(jìn)行一次檢查上報(bào)邏輯 -
4、檢查上報(bào)邏輯
checkAndPostEvent(mRecyclerView)public void checkAndPostEvent(RecyclerView recyclerView) { if (recyclerView == null) { return; } RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager == null) { return; } int newFirstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); int newLastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); if (newFirstVisibleItemPosition == -1 || newLastVisibleItemPosition == -1) { return; } //這里可以插入判斷第一個(gè) item 和最后一個(gè) item 可見百分比的邏輯 for (int i = newFirstVisibleItemPosition; i <= newLastVisibleItemPosition; i++) { RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(i); if (viewHolder == null) { continue; } int bindingAdapterPosition = viewHolder.getBindingAdapterPosition() - getHeaderPositionCount(); int absoluteAdapterPosition = viewHolder.getAbsoluteAdapterPosition() - getHeaderPositionCount(); if (bindingAdapterPosition < 0 || absoluteAdapterPosition < 0 || !isBindingAdapter(viewHolder)) { continue; } T entity = getAdapterEntityForPosition(bindingAdapterPosition, viewHolder); if (entity != null && needPostEvent(entity)) { putEntity(entity, absoluteAdapterPosition, bindingAdapterPosition); } } }這里主要是判斷當(dāng)前 RecyclerView 中第一個(gè)可見項(xiàng)和最后一個(gè)可見項(xiàng)的區(qū)間,將區(qū)間內(nèi)的 item 通過調(diào)用
putEntity收集到待上報(bào)集合中。這里可以插入 item 是否滿足可見條件的邏輯(putEntity()調(diào)用之前即可),判斷第一個(gè) item 和最后一個(gè) item 可見百分比即可,比如獲取newFirstVisibleItemPositionitem 后,通過調(diào)用recyclerView.findViewHolderForAdapterPosition(newFirstVisibleItemPosition)獲取它的ViewHolder從而獲取 View,通過view.getGlobalVisibleRect()方法獲取其所在Rect位置,通過與RecyclerView容器的Rect對(duì)比,可知當(dāng)前狀態(tài)下,newFirstVisibleItemPosition這個(gè)item可見百分比是多少。RecyclerViewExposure 選擇不支持該功能,有需要的同學(xué)可以自己擴(kuò)展實(shí)現(xiàn)。 -
5、
putEntity():收集數(shù)據(jù)方法/** * 數(shù)據(jù)定義 * * @apiNote Triple 是包含 entity absoluteAdapterPosition bindingAdapterPosition 的數(shù)據(jù)類 * @apiNote entity entity,列表 Item 的數(shù)據(jù) * @apiNote absoluteAdapterPosition 相對(duì) RecyclerView 的 item position * @apiNote bindingAdapterPosition 相對(duì)子 Adapter級(jí)別 item position */ private final Map<String, Triple<T, Integer, Integer>> mPostEventDataHashMap = new LinkedHashMap<>(); /** * 發(fā)送事件的 Runnable */ private final Runnable mPostEventRunnable = this::postEvent; /** * @param entity entity * @param absoluteAdapterPosition 相對(duì)RecycleView的位置 */ private void putEntity(@NonNull T entity, int absoluteAdapterPosition, int bindingAdapterPosition) { String id = entity.getIdForImpr(); // Log.d(TAG, "putEntity--id:" + id + ", bindingAdapterPosition:" + bindingAdapterPosition); if (TextUtils.isEmpty(id)) { return; } mPostEventDataHashMap.put(id, new Triple<>(entity, absoluteAdapterPosition, bindingAdapterPosition)); UIHelper.removeCallback(mPostEventRunnable); UIHelper.runOnUiThreadDelay(mPostEventRunnable, POST_EVENT_DEBOUNCE); }符合一定條件之后,putEntity 會(huì)被調(diào)用,將數(shù)據(jù)塞到 mPostEventDataHashMap 中,同時(shí)開啟一個(gè)定時(shí),時(shí)間為 600ms,結(jié)束將調(diào)用
mPostEventRunnable去執(zhí)行postEvent()從而發(fā)送事件。 -
6、
postEvent():發(fā)送曝光事件private void postEvent() { Map<String, Triple<T, Integer, Integer>> backupMap = new LinkedHashMap<>(mPostEventDataHashMap); mPostEventDataHashMap.clear(); ThreadHelper.executeExposureSingleTask(() -> { List<Triple<T, Integer, Integer>> tripleList = new ArrayList<>(); for (Map.Entry<String, Triple<T, Integer, Integer>> stringPairEntry : backupMap.entrySet()) { Triple<T, Integer, Integer> value = stringPairEntry.getValue(); T entity = value.getFirst(); if (entity == null) { continue; } //單個(gè)曝光 onItemExposure(entity, value.getSecond(), value.getThird()); tripleList.add(value); } //批量曝光 onBatchItemExposure(tripleList); }); }延遲結(jié)束時(shí)將執(zhí)行發(fā)送數(shù)據(jù)的邏輯,主要是遍歷
mPostEventDataHashMap集合,將數(shù)據(jù)通過onItemExposure()進(jìn)行單個(gè)曝光和批量曝光onBatchItemExposure() -
7、
onChildViewAttachedToWindow():RecyclerView Item View 首次加載到屏幕觸發(fā)@Override public void onChildViewAttachedToWindow(@NonNull View view) { if (mRecyclerView == null) { return; } RecyclerView.ViewHolder viewHolder = mRecyclerView.findContainingViewHolder(view); if (viewHolder == null) { return; } int bindingAdapterPosition = -1; try { bindingAdapterPosition = viewHolder.getBindingAdapterPosition() - getHeaderPositionCount(); } catch (Exception exception) { exception.printStackTrace(); } int absoluteAdapterPosition = viewHolder.getAbsoluteAdapterPosition() - getHeaderPositionCount(); if (bindingAdapterPosition < 0 || absoluteAdapterPosition < 0) { return; } if (isBindingAdapter(viewHolder)) { T entity = getAdapterEntityForPosition(bindingAdapterPosition, viewHolder); if (entity == null) { return; } if (needPostEvent(entity)) { putEntity(entity, absoluteAdapterPosition, bindingAdapterPosition); } } }其實(shí)簡(jiǎn)簡(jiǎn)單單的獲取指定 view 數(shù)據(jù)和添加可見數(shù)據(jù)到集合,需要注意的是,假設(shè)需要要求 view 曝光百分之 xx 才算曝光,那么在 putEntity 之前需要判斷當(dāng)前 item 相對(duì)于 RecyclerView 的百分比
-
8、
onChildViewDetachedFromWindow()@Override public void onChildViewDetachedFromWindow(@NonNull View view) { if (mRecyclerView == null) { return; } RecyclerView.ViewHolder viewHolder = mRecyclerView.findContainingViewHolder(view); if (viewHolder == null) { return; } int bindingAdapterPosition = viewHolder.getBindingAdapterPosition() - getHeaderPositionCount(); int absoluteAdapterPosition = viewHolder.getAbsoluteAdapterPosition() - getHeaderPositionCount(); if (bindingAdapterPosition < 0 || absoluteAdapterPosition < 0) { return; } if (isBindingAdapter(viewHolder)) { T entity = getAdapterEntityForPosition(bindingAdapterPosition, viewHolder); if (entity == null) { return; } removeEntity(entity); } }這里主要是
removeEntity()邏輯,在 view 移開屏幕的時(shí)候觸發(fā),并且這里會(huì)重置postEvent()的倒計(jì)時(shí)/** * @param entity entity */ private void removeEntity(T entity) { mPostEventDataHashMap.remove(entity.getIdForImpr()); UIHelper.removeCallback(mPostEventRunnable); UIHelper.runOnUiThreadDelay(mPostEventRunnable, POST_EVENT_DEBOUNCE); }
3、針對(duì) ListAdapter 精簡(jiǎn)使用方法
- 由于 ListAdapter 的數(shù)據(jù)源固定為
getItem(),RecyclerViewExposure 擴(kuò)展了AbsListAdapterImprEventHelper類,使 ListAdapter 的列表曝光只需要關(guān)注 needPostEvent() 和相關(guān)曝光方法
abstract class AbsListAdapterImprEventHelper<L : ListAdapter<T, RecyclerView.ViewHolder>, T : IEntityForImpr> :
AbsListImprEventHelper<L, T> {
constructor(
recyclerView: RecyclerView,
componentActivity: ComponentActivity
) : super(recyclerView, componentActivity)
constructor(
recyclerView: RecyclerView,
fragment: Fragment
) : super(recyclerView, fragment)
@Suppress("UNCHECKED_CAST", "IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE")
override fun getAdapterEntityForPosition(
bindingAdapterPosition: Int,
viewHolder: RecyclerView.ViewHolder
): T? {
return (viewHolder.bindingAdapter as? L)?.let {
it.currentList.run {
if (bindingAdapterPosition >= this.size) {
return null
}
return this[bindingAdapterPosition]
}
}
}
}
-
AbsListAdapterImprEventHelper的一個(gè)使用例子
class ListAdapterImprEventHelper(
recyclerView: RecyclerView,
fragment: Fragment
) : AbsListAdapterImprEventHelper<ItemRecyclerViewListAdapter,
PlaceholderContent.PlaceholderItem>(
recyclerView,
fragment
) {
override fun needPostEvent(entity: PlaceholderContent.PlaceholderItem): Boolean {
return true
}
override fun onItemExposure(
entity: PlaceholderContent.PlaceholderItem,
absoluteAdapterPosition: Int,
bindingAdapterPosition: Int
) {
Log.d(
"ListAdapterImprEventHelper",
"onItemExposure:---- absoluteAdapterPosition:$absoluteAdapterPosition ,$entity"
)
}
}
四、總結(jié)
- Gradle 插件編譯插樁(ASM/Javassist) 是很強(qiáng)大的工具,除了本篇提到的替換繼承類的功能外,只能用為所欲為來形容它們了。比如常見的項(xiàng)目線程池問題的治理,測(cè)試mock 數(shù)據(jù),ARouter 中路由表的生成和初始化,AndResGuard 中對(duì)資源路徑的縮減,對(duì)三方庫(kù)中混亂調(diào)用系統(tǒng) API 獲取敏感信息進(jìn)行治理等等。ASM 性能優(yōu)秀但是入手難度大,需要開發(fā)者熟悉 Class 文件體系,JVM 指令集,ASM API 的使用(Visitor 模式),這也要求開發(fā)者需要有相關(guān)的基礎(chǔ)知識(shí)儲(chǔ)備,不然很容易玩不下去。

