目录
  1. 1. 一、Activity 页面浏览采集
  2. 2. 二、Fragment 页面浏览采集
  3. 3. 三、ViewPager Fragment 的去重处理
  4. 4. 四、页面参数采集
  5. 5. 面试常考问题
【全埋点方案系列】AppViewScreen全埋点

页面浏览(Screen View)是用户行为分析的基本维度。全埋点需要在无业务代码侵入的情况下,自动采集每个 Activity 和 Fragment 的页面曝光、离开事件,并计算页面停留时长。

一、Activity 页面浏览采集

通过 ActivityLifecycleCallbacks 即可覆盖所有 Activity 的生命周期:

class ScreenTracker : Application.ActivityLifecycleCallbacks {

private val screenStack = mutableMapOf<String, Long>() // 页面名 → 进入时间戳
private var currentScreen = ""

override fun onActivityResumed(activity: Activity) {
val screenName = activity.javaClass.simpleName
val screenClass = activity.javaClass.name

// 如果切换到了新页面,先结束上一个页面的计时
if (currentScreen.isNotEmpty() && currentScreen != screenClass) {
trackScreenEnd(currentScreen)
}

currentScreen = screenClass
screenStack[screenClass] = System.currentTimeMillis()

// 上报页面浏览事件
AnalyticsSDK.track("screen_view", mapOf(
"screen_name" to screenName,
"screen_class" to screenClass,
"title" to (activity.title?.toString() ?: ""),
"intent_extras" to activity.intent?.extras?.keySet()?.joinToString()
))
}

override fun onActivityPaused(activity: Activity) {
// 页面被覆盖(如跳转其他 Activity)
val screenClass = activity.javaClass.name
if (currentScreen == screenClass) {
trackScreenEnd(screenClass)
}
}

private fun trackScreenEnd(screenClass: String) {
val enterTime = screenStack[screenClass] ?: return
val duration = System.currentTimeMillis() - enterTime
AnalyticsSDK.track("screen_leave", mapOf(
"screen_class" to screenClass,
"duration_ms" to duration
))
screenStack.remove(screenClass)
}
}

二、Fragment 页面浏览采集

Fragment 的生命周期独立于 Activity,需要使用 FragmentLifecycleCallbacks

class FragmentScreenTracker : FragmentManager.FragmentLifecycleCallbacks() {

private val fragmentTimestamps = mutableMapOf<String, Long>()

override fun onFragmentResumed(fm: FragmentManager, fragment: Fragment) {
val screenName = fragment.javaClass.simpleName
val key = "${fragment.hashCode()}_$screenName"
fragmentTimestamps[key] = System.currentTimeMillis()

// 找到宿主 Activity 组合为完整页面路径
val pagePath = "${fragment.requireActivity().javaClass.simpleName}/${screenName}"

AnalyticsSDK.track("screen_view", mapOf(
"screen_name" to screenName,
"page_path" to pagePath,
"is_fragment" to true,
"parent_activity" to fragment.requireActivity().javaClass.simpleName
))
}

override fun onFragmentPaused(fm: FragmentManager, fragment: Fragment) {
val key = "${fragment.hashCode()}_${fragment.javaClass.simpleName}"
val enterTime = fragmentTimestamps[key] ?: return
val duration = System.currentTimeMillis() - enterTime

AnalyticsSDK.track("screen_leave", mapOf(
"screen_name" to fragment.javaClass.simpleName,
"duration_ms" to duration
))
fragmentTimestamps.remove(key)
}
}

// 在 Application 中全局注册
class App : Application() {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
activity.supportFragmentManager
.registerFragmentLifecycleCallbacks(FragmentScreenTracker(), true)
}
})
}
}

三、ViewPager Fragment 的去重处理

ViewPager 会预加载相邻页面的 Fragment,导致 onFragmentResumed 在 Fragment 真正可见时触发,但若用户未滑动到该页面,埋点会采集到未曝光的页面。

class ViewPagerAwareTracker : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentResumed(fm: FragmentManager, fragment: Fragment) {
// 通过 ViewPager 的 primaryItem 确认是否真正可见
val isPrimary = fragment.parentFragment?.let { parent ->
parent.view?.findViewById<ViewPager>(R.id.view_pager)?.let { vp ->
val currentItem = vp.currentItem
val adapter = vp.adapter ?: return@let false
// 比较当前 Fragment 是否为 primary item
fragment == adapter.instantiateItem(vp, currentItem)
}
} ?: true // 非 ViewPager 场景,直接上报

if (isPrimary) {
trackScreenView(fragment)
}
}
}

四、页面参数采集

除了页面名称和时间,完整的 page_path 可包含更多上下文:

fun buildPageContext(activity: Activity, fragment: Fragment? = null): Map<String, Any?> {
return buildMap {
put("activity", activity.javaClass.simpleName)
put("fragment", fragment?.javaClass?.simpleName)
put("referrer", activity.intent?.getStringExtra("referrer"))
put("from_notification", activity.intent?.hasExtra("notification_id") == true)
put("from_deep_link", activity.intent?.action == Intent.ACTION_VIEW)

// Android framework 提供了 activity 堆栈信息
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
put("task_id", activity.taskId)
}
}
}

AOSP 中 Activity 生命周期回调通过 Application.ActivityLifecycleCallbacks 接口定义,实际回调由 ActivityThread 触发,源码路径:frameworks/base/core/java/android/app/ActivityThread.java 中的 performResumeActivity() / performPauseActivity()


面试常考问题

Q1:Fragment 的 onResume 和 Activity 的 onResume 顺序是什么?如何避免重复上报?

顺序:Fragment.onResume → Activity.onResume。在 Activity 的 onActivityResumed 回调中上报 Activity 的 screen_view,在 Fragment 的 onFragmentResumed 中上报 Fragment 的 screen_view,两者使用不同的 page_path 层级防止去重覆盖。上报时带上 parent_activity 字段建立关联。

Q2:如何处理屏幕旋转导致的重复 screen_view?

旋转时 Activity 重建,onPause → onResume 会再次触发。可以通过对比 savedInstanceState 判断:若 savedInstanceState != null 且上一个 screen 的类名与当前相同,则为重建而非新页面跳转,可跳过 screen_view 上报。也可通过 ViewModel 在配置变更期间的存活特性来标记”已上报”状态。

Q3:Dialog/DialogFragment 算独立页面吗?

取决于分析需求。Dialog 通常不视为独立页面,但要单独统计曝光。Dialog 的 show/dismiss 可通过 FragmentManager.FragmentLifecycleCallbacks(DialogFragment)或自定义 DialogLifecycleCallbacks 来跟踪。如果 Dialog 是弹窗广告或重要交互,建议单独作为一种 screen_type=overlay 上报。

打赏
  • 微信
  • 支付宝

评论