Android 页面曝光埋点难点及方案
曝光埋点分为两种:
PV show
概念不同:PV = Page View,它特指页面维度的展示。对于 Android 平台来说,可以是一个 Activity 或 Fragment。而 show 可以是任何东西的展示,可以是页面,也可以是一个控件的展示。 上报时机不同:PV 是在离开页面的时候上报,show 是在控件展示的时候上报。 上报参数不同:PV 通常会上报页面停留时长。 消费场景不同:在消费侧,“展示”通常用于形成页面转化率漏斗,PV 和 show 都可用于形成这样的漏斗。但 show 比 PV 更精细,因为可能 A 页面中有 N 个入口可以通往 B页面。
由于产品希望知道更精确的入口信息,遂新增埋点全都是 show。
public interface PvTracker {
String getPvEventId();// 生成事件ID
Bundle getPvExtra();// 生成额外参数
default boolean shouldReport() {return true;}
default String getUniqueKey() {return null;}
}
class AvatarActivity : BaseActivity, PvTracker{
override fun getPvEventId() = "avatar.pv"
override fun getPvExtra() = Bundle()
}然后该 pvtracker 库就会自动实现 Activity 的 PV 上报。
class PvLifeCycleCallback implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityResumed(Activity activity) {
String eventId = getEventId(activity);
if (!TextUtils.isEmpty(eventId)) {
onActivityVisibleChanged(activity, true); // activity 可见
}
}@Override
public void onActivityPaused(Activity activity) {
String eventId = getEventId(activity);
if (!TextUtils.isEmpty(eventId)) {
onActivityVisibleChanged(activity, false);// activity 不可见
}
}// 当 Activity 可见性发生变化
private void onActivityVisibleChanged(Activity activity, boolean isVisible) {
if (activity instanceof PvTracker) {
PvTracker tracker = (PvTracker) activity;
if (!tracker.shouldReport()) {
return;
}
String eventId = tracker.getPvEventId();
Bundle bundle = tracker.getPvExtra();if (TextUtils.isEmpty(eventId) || mActivityLoadType == null) {
return;
}
String uniqueEventId = PageViewTracker.getUniqueId(activity, eventId);
if (isVisible) {
// 标记曝光开始
PvManager.getInstance().triggerVisible(uniqueEventId, eventId, bundle, loadType);
} else {
// 标记曝光结束,统计曝光时间并上报PV
PvManager.getInstance().triggerInvisible(uniqueEventId);
}
}
}
}
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 在 Application 中初始化
registerActivityLifecycleCallbacks(PvLifeCycleCallback)
}
}
Fragment PV
public abstract static class FragmentLifecycleCallbacks {
public void onFragmentPreAttached(@NonNull FragmentManager fm, @NonNull Fragment f,@NonNull Context context) {}
public void onFragmentAttached(@NonNull FragmentManager fm, @NonNull Fragment f,@NonNull Context context) {}
public void onFragmentPreCreated(@NonNull FragmentManager fm, @NonNull Fragment f,@Nullable Bundle savedInstanceState) {}
public void onFragmentCreated(@NonNull FragmentManager fm, @NonNull Fragment f,@Nullable Bundle savedInstanceState) {}
public void onFragmentActivityCreated(@NonNull FragmentManager fm, @NonNull Fragment f,@Nullable Bundle savedInstanceState) {}
public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f,@NonNull View v, @Nullable Bundle savedInstanceState) {}
public void onFragmentStarted(@NonNull FragmentManager fm, @NonNull Fragment f) {}
public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f) {}
public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {}
public void onFragmentStopped(@NonNull FragmentManager fm, @NonNull Fragment f) {}
public void onFragmentSaveInstanceState(@NonNull FragmentManager fm, @NonNull Fragment f,@NonNull Bundle outState) {}
public void onFragmentViewDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {}
public void onFragmentDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {}
public void onFragmentDetached(@NonNull FragmentManager fm, @NonNull Fragment f) {}
}
可以通过观察者模式在 Fragment 实例以外的地方全局性地监听所有 Fragment 的生命周期。当其中的 onFragmentResumed() 回调时,意味着 Fragment 可见,而当 onFragmentPaused()回调时,意味着 Fragment 不可见。
调用 FragmentTransaction的 show()/hide() 方法时,不会走对应的 resume/pause 生命周期回调。(因为它只是隐藏了 Fragment 对应的 View,但 Fragment 还处于 resume 状态,详见FragmentTransaction.hide()- findings | by Nav Singh 🇨🇦 | Nerd For Tech | Medium)
https://medium.com/nerd-for-tech/fragmenttransaction-hide-findings-5b998e959c82
当 Fragment 和 ViewPager/ViewPager2 共用时,resume/pause 生命周期回调失效。表现为没有展示的 Fragment 会回调 resume,而不可见的 Fragment 不会回调 pause。
通过监听 ViewPager 页面切换来实现 Fragment + ViewPager 的可见性判断:在 ViewPager 初始化完毕后调用 PageViewTracker.getInstance().observePageChange(viewpager) 如果 ViewPager + Fragment 嵌套在一个父 Fragment 还需在父 Fragment.onHiddenChanged() 方法里监听父 Fragment 的显示隐藏状态。
pvTracker 的解决方案是“把皮球踢给上层”,即上层手动调用一个方法来告知库当前 Fragment 的可见性。
pvtracker 是“半声明式 PV 上报”(Fragment 的可见性需要上层调方法)。
是不是可以忘掉 Fragment,通过判定其根视图的可见性来表达 Fragment 的可见性?
所以需要一个控件维度全局可见性监听器,引用全网最优雅安卓控件可见性检测 中提供的解决方案:
https://juejin.cn/post/7165427955902971918
fun View.onVisibilityChange(
viewGroups: List<ViewGroup> = emptyList(), // 会被插入 Fragment 的容器集合
needScrollListener: Boolean = true,
block: (view: View, isVisible: Boolean) -> Unit
) {
val KEY_VISIBILITY = "KEY_VISIBILITY".hashCode()
val KEY_HAS_LISTENER = "KEY_HAS_LISTENER".hashCode()
// 若当前控件已监听可见性,则返回
if (getTag(KEY_HAS_LISTENER) == true) return // 检测可见性
val checkVisibility = {
// 获取上一次可见性
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
// 判断控件是否出现在屏幕中
val isInScreen = this.isInScreen
// 首次可见性变更
if (lastVisibility == null) {
if (isInScreen) {
block(this, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非首次可见性变更
else if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
}
// 全局重绘监听器
class LayoutListener : ViewTreeObserver.OnGlobalLayoutListener {
// 标记位用于区别是否是遮挡case
var addedView: View? = null
override fun onGlobalLayout() {
// 遮挡 case
if (addedView != null) {
// 插入视图矩形区域
val addedRect = Rect().also { addedView?.getGlobalVisibleRect(it) }
// 当前视图矩形区域
val rect = Rect().also { this@onVisibilityChange.getGlobalVisibleRect(it) }
// 如果插入视图矩形区域包含当前视图矩形区域,则视为当前控件不可见
if (addedRect.contains(rect)) {
block(this@onVisibilityChange, false)
setTag(KEY_VISIBILITY, false)
} else {
block(this@onVisibilityChange, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非遮挡 case
else {
checkVisibility()
}
}
}
val layoutListener = LayoutListener()
// 编辑容器监听其插入视图时机
viewGroups.forEachIndexed { index, viewGroup ->
viewGroup.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
// 当控件插入,则置标记位
layoutListener.addedView = child
}
override fun onChildViewRemoved(parent: View?, child: View?) {
// 当控件移除,则置标记位
layoutListener.addedView = null
}
})
}
viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
// 全局滚动监听器
var scrollListener:ViewTreeObserver.OnScrollChangedListener? = null
if (needScrollListener) {
scrollListener = ViewTreeObserver.OnScrollChangedListener { checkVisibility() }
viewTreeObserver.addOnScrollChangedListener(scrollListener)
}
// 全局焦点变化监听器
val focusChangeListener = ViewTreeObserver.OnWindowFocusChangeListener { hasFocus ->
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
val isInScreen = this.isInScreen
if (hasFocus) {
if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
} else {
if (lastVisibility == true) {
block(this, false)
setTag(KEY_VISIBILITY, false)
}
}
}
viewTreeObserver.addOnWindowFocusChangeListener(focusChangeListener)
// 为避免内存泄漏,当视图被移出的同时反注册监听器
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}
override fun onViewDetachedFromWindow(v: View?) {
v ?: return
// 有时候 View detach 后,还会执行全局重绘,为此退后反注册
post {
try {
v.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
} catch (_: java.lang.Exception) {
v.viewTreeObserver.removeGlobalOnLayoutListener(layoutListener)
}
v.viewTreeObserver.removeOnWindowFocusChangeListener(focusChangeListener)
if(scrollListener !=null) v.viewTreeObserver.removeOnScrollChangedListener(scrollListener)
viewGroups.forEach { it.setOnHierarchyChangeListener(null) }
}
removeOnAttachStateChangeListener(this)
}
})
// 标记已设置监听器
setTag(KEY_HAS_LISTENER, true)
}
有了这个扩展方法,就可以在项目中的 BaseFragment 中进行全局 Fragment 的可见性监听了:
// 抽象 Fragment
abstract class BaseFragment:Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if(detectVisibility){
view.onVisibilityChange { view, isVisible ->
onFragmentVisibilityChange(isVisible)
}
}
}
// 抽象属性:是否检测当前 fragment 的可见性
abstract val detectVisibility: Boolean
open fun onFragmentVisibilityChange(show: Boolean) {}
}
class FragmentA: BaseFragment() {
override val detectVisibility: Boolean
get() = true
override fun onFragmentVisibilityChange(show: Boolean) {
if(show) ... else ...
}
}
interface ExposureParam {
val eventId: String
fun getExtra(): Map<String, String?> = emptyMap()
fun isForce():Boolean = false
}
class MaterialFragment : BaseFragment(), ExposureParam {
abstract val tabName: String
abstract val type: Intoverride val eventId: String
get() = "material.show"override fun getExtra(): Map<String, String?> {
return mapOf(
"tab_name" to tabName,
"type" to type.toString()
)
}
}
class PageVisibilityListener : Application.ActivityLifecycleCallbacks {
// 页面可见性变化回调
var onPageVisibilityChange: ((page: Any, isVisible: Boolean) -> Unit)? = null private val fragmentLifecycleCallbacks by lazy(LazyThreadSafetyMode.NONE) {
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
// 注册 Fragment 根视图可见性监听器
if (f is ExposureParam) {
v.onVisibilityChange { view, isVisible ->
onPageVisibilityChange?.invoke(f, isVisible)
}
}
}
}
}
override fun onActivityCreated(activity: Activity, p1: Bundle?) {
// 注册 Fragment 生命周期监听器
(activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
override fun onActivityDestroyed(activity: Activity) {
// 注销 Fragment 生命周期监听器
(activity as? FragmentActivity)?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
}
override fun onActivityStarted(p0: Activity) {
}
override fun onActivityResumed(activity: Activity) {
// activity 可见
if (activity is ExposureParam) onPageVisibilityChange?.invoke(activity, true)
}
override fun onActivityPaused(activity: Activity) {
// activity 不可见
if (activity is ExposureParam) onPageVisibilityChange?.invoke(activity, false)
}
override fun onActivityStopped(p0: Activity) {
}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
}
}
该监听器同时监听了 Activity 和 Fragment 的可见性变化。其中 Activity 的可见性变化是借助于 ActivityLifecycleCallbacks,而 Fragment 的可见性变化是借助于其视图的可见性。
open class MyApplication : Application(){
private val fragmentVisibilityListener by lazy(LazyThreadSafetyMode.NONE) {
PageVisibilityListener().apply {
onPageVisibilityChange = { page, isVisible ->
// 当页面可见时,上报 show
if (isVisible) {
(page as? ExposureParam)?.also { param ->
ReportUtil.reportShow(param.isForce(), param.eventId, param.getExtra())
}
}
}
}
} override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(fragmentVisibilityListener)
}
这样一来,上报时机已经完全自动化,只需要在上报的页面通过 ExposureParam 声明上报参数即可。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读: