页面曝光难点分析及应对方案

简介: 页面曝光难点分析及应对方案

曝光


曝光埋点分为两种:


  1. PV


  1. show


它俩都表示“展示”,但有如下不同:


  • 概念不同:PV = Page View,它特指页面维度的展示。对于 Android 平台来说,可以是一个 Activity 或 Fragment。而 show 可以是任何东西的展示,可以是页面,也可以是一个控件的展示。


  • 上报时机不同:PV 是在离开页面的时候上报,show 是在控件展示的时候上报。


  • 上报参数不同:PV 通常会上报页面停留时长。


  • 消费场景不同:在消费侧,“展示”通常用于形成页面转化率漏斗,PV 和 show 都可用于形成这样的漏斗。但 show 比 PV 更精细,因为可能 A 页面中有 N 个入口可以通往 B页面。


由于产品希望知道更精确的入口信息,遂新增埋点全都是 show。


现有 PV 上报组件


Activity PV


项目中引入了一个第三方库实现了 Activity PV 半自动化上报:


public interface PvTracker {
    String getPvEventId();// 生成事件ID
    Bundle getPvExtra();// 生成额外参数
    default boolean shouldReport() {return true;}
    default String getUniqueKey() {return null;}
}


该接口定义了如何生成曝光埋点的事件ID和额外参数。


当某 Activity 需要 PV 埋点时实现该接口:


class AvatarActivity : BaseActivity, PvTracker{
    override fun getPvEventId() = "avatar.pv"
    override fun getPvExtra() = Bundle()
}


然后该 pvtracker 库就会自动实现 Activity 的 PV 上报。


它通过如下方式对全局 Activity 生命周期做了监听:


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);
            }
        }
    }
}


PvLifeCycleCallback 是一个全局性的 Activity 生命周期监听器,它会在 Application 初始化的时候注册:


class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 在 Application 中初始化
        registerActivityLifecycleCallbacks(PvLifeCycleCallback)
    }
}


这套方案实现了 Activity 层面半自动声明式埋点,即只需要编码埋点数据,不需要手动触发埋点。


Fragment PV


Fragment 生命周期是件非常头痛的事情。


FragmentManager.FragmentLifecycleCallbacks出现之前没有一个官方的解决方案,Fragment 生命周期处于一片混沌之中。


FragmentManager.FragmentLifecycleCallbacks 为开发者开了一扇窗(但这是一扇破窗):


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 不可见。


但有如下例外情况:


  1. 调用 FragmentTransaction的 show()/hide() 方法时,不会走对应的 resume/pause 生命周期回调。(因为它只是隐藏了 Fragment 对应的 View,但 Fragment 还处于 resume 状态,详见FragmentTransaction.hide()- findings | by Nav Singh 🇨🇦 | Nerd For Tech | Medium


  1. 当 Fragment 和 ViewPager/ViewPager2 共用时,resume/pause 生命周期回调失效。表现为没有展示的 Fragment 会回调 resume,而不可见的 Fragment 不会回调 pause。


pvTracker 的这个库在检测 Fragment 生命周期时也有上述问题。不过它也给出了解决方案:


  • 通过监听 ViewPager 页面切换来实现 Fragment + ViewPager 的可见性判断:在 ViewPager 初始化完毕后调用 PageViewTracker.getInstance().observePageChange(viewpager)
  • 如果 ViewPager + Fragment 嵌套在一个父 Fragment 还需在父 Fragment.onHiddenChanged() 方法里监听父 Fragment 的显示隐藏状态。


pvTracker 的解决方案是“把皮球踢给上层”,即上层手动调用一个方法来告知库当前 Fragment 的可见性。


全声明式 show 上报


pvtracker 是“半声明式 PV 上报”(Fragment 的可见性需要上层调方法)。


缺少一种“全声明式 show 上报”,即上层无需关注任何上报时机,只需生成埋点参数,就能自动实现 show 的上报。


Fragment 之所以会出现上述例外的情况,是因为 Fragment 的生命周期和其根视图的生命周期不同步。


是不是可以忘掉 Fragment,通过判定其根视图的可见性来表达 Fragment 的可见性?


所以需要一个控件维度全局可见性监听器,引用全网最优雅安卓控件可见性检测 中提供的解决方案:


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) {}
}


其子类必须实现抽象属性detectVisibility,表示是否监听当前Fragment的可见性:


class FragmentA: BaseFragment() {
    override val detectVisibility: Boolean
        get() = true
    override fun onFragmentVisibilityChange(show: Boolean) {
        if(show) ... else ...
    }
}


为了让 show 上报不入侵基类,选择了一种可拔插的方案,先定义一个接口:


interface ExposureParam {
    val eventId: String
    fun getExtra(): Map<String, String?> = emptyMap()
    fun isForce():Boolean = false
}


该接口用于生成 show 上报的参数。任何需要上报 show 的页面都可以实现该接口:


class MaterialFragment : BaseFragment(), ExposureParam {
    abstract val tabName: String
    abstract val type: Int
    override val eventId: String
        get() = "material.show"
    override fun getExtra(): Map<String, String?> {
        return mapOf(
            "tab_name" to tabName,
            "type" to type.toString()
        )
    }
}


再自定义一个 Activity 生命周期监听器:


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 的可见性变化是借助于其视图的可见性。


Activity 和 Fragment 的可见性监听使用同一个onPageVisibilityChange进行回调。

然后在 Application 中页面可见性监听器:


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 声明上报参数即可。


推荐阅读


业务代码参数透传满天飞?(一)


业务代码参数透传满天飞?(二)


全网最优雅安卓控件可见性检测


全网最优雅安卓列表项可见性检测


页面曝光难点分析及应对方案


你的代码太啰嗦了 | 这么多对象名?


你的代码太啰嗦了 | 这么多方法调用?


目录
相关文章
|
9月前
|
监控 BI 定位技术
直播程序源码开发建设:洞察全局,数据统计与分析功能
数据统计与分析功能不管是对直播程序源码平台的主播或运营者都会有极大的帮助,是了解观众需求、优化用户体验成为直播平台发展的关键功能,这也是开发搭建直播程序源码平台的必备功能之一。
直播程序源码开发建设:洞察全局,数据统计与分析功能
|
移动开发 JavaScript 前端开发
报错/卡顿是衡量产品体验的基本要素
报错/卡顿是衡量产品体验的基本要素
79 0
报错/卡顿是衡量产品体验的基本要素
|
数据采集 监控 前端开发
网站流量日志分析背景介绍- - 如何进行网站分析--流量分析(质量、多维细分)|学习笔记
快速学习网站流量日志分析背景介绍- -如何进行网站分析--流量分析(质量、多维细分)
191 0
网站流量日志分析背景介绍- - 如何进行网站分析--流量分析(质量、多维细分)|学习笔记
|
安全 搜索推荐 大数据
精准大数据获客如何做到APP直抓 精准截流 提高客户质量
在公司市场竞争的大环境中,许多公司为了更好地存活,迫不得已去寻找新的销售渠道,稍不留神被坑上当受骗的事儿也常常产生。现阶段在销售市场上充溢这许多披上大数据“外套”的知名品牌,去做销售市场,乃至还存有违法违纪的个人行为存有。为了更好地更强的服务型,为大量创业人出示更强领域数据解决方法,运营商大数据就应时而生。
精准大数据获客如何做到APP直抓 精准截流 提高客户质量
|
数据挖掘 测试技术 数据处理
数据分析实战 | A/B测试探寻哪种广告点击率更高?
数据分析实战 | A/B测试探寻哪种广告点击率更高?
数据分析实战 | A/B测试探寻哪种广告点击率更高?
|
分布式计算 监控 前端开发
拍卖前端质量之 基于业务驱动的前端性能监控的有效实践
前端的本质价值是什么? 我认为是 给用户创造良好的交互体验。 前端性能对用户体验、对业务跳失率的影响,在业界已有共识,不言而喻。 以下详述测试视角,前端性能优化的解法,简言之即:从发现、分析、验证3方面驱动推进页面性能优化 并通过实际案例更生动描述。
349 1
|
缓存 编解码 人工智能
移动端UI自动化过程中的难点及应对策略
在之前的文章《自动化质量评估维度》中,我们探讨了衡量自动化稳定性的误报率指标,今天重点针对移动端UI自动化过程中导致误报的几个难点进行展开分析并给出相应的解决方案。
563 1
|
运维 监控 Java
助你秒级定位线上问题!
经常做后端服务开发的同学,或多或
助你秒级定位线上问题!
|
新零售 存储 前端开发
精准投放能力揭秘,带你实现业务更多可能性!
在过去的近一年时间里,围绕淘系营销导购的投放能力建设,在“一个淘系、一套体系”的原则下,通过整合原天猫、淘宝、聚划算等的投放系统,使得技术层面可以站在更高的角度,面向新零售进行投放域的支撑与保障,在各个垂直能力建设上做深做强,并服务于集团更为广泛的业务方。 定向投放(后文简称“定投”)的能力建设,在大的投放技术体系内,“一心一役”与各核心应用深度融合与协同,完成了较大的技术升级,服务了淘系(淘宝、天猫、大聚划算)、天猫进出口、天猫超市、AE等业务,帮助业务实现了更多的可能性。
1711 0
精准投放能力揭秘,带你实现业务更多可能性!
|
数据采集 存储 监控
亿级搜索系统的基石,如何保障实时数据质量?
突然而至的疫情,让线下娱乐几乎停摆。全国人民对于线上娱乐需求激增,在家追剧、在家上课、在家互动,还要时刻关注疫情动态。每时每刻,都有海量用户在优酷搜索自己想看的内容。千万级别的视频量,几十亿级别的信息量,如何能做到将信息及时有效的透出给用户?怎样保障数据准确无误的呈现呢?
1235 0
亿级搜索系统的基石,如何保障实时数据质量?