本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

曝光

曝光埋点分为两种:

  1. PV
  2. 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)
  2. 当 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 声明上报参数即可。

引荐阅览

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

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

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

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

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

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

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