目录
  1. 1. 一、AOSP 事件分发机制回顾
    1. 1.1. 1.1 ViewGroup.dispatchTouchEvent 源码分析
    2. 1.2. 1.2 事件穿透的条件
  2. 2. 二、完整实现
    1. 2.1. 2.1 TrackingOverlay 实现
    2. 2.2. 2.2 OverlayConfig 配置
    3. 2.3. 2.3 信息采集器
  3. 3. 三、全局注入方案
    1. 3.1. 3.1 自动添加透明层
    2. 3.2. 3.2 Application 注册
  4. 4. 四、架构流程图
  5. 5. 五、高级场景处理
    1. 5.1. 5.1 RecyclerView 快速滚动优化
    2. 5.2. 5.2 Dialog / PopupWindow 覆盖
    3. 5.3. 5.3 分屏 / 多窗口模式适配
  6. 6. 六、性能优化
    1. 6.1. 6.1 坐标查找优化
  7. 7. 七、与手势导航栏的冲突处理
  8. 8. 八、与其他方案组合
  9. 9. 九、ProGuard/R8 规则
  10. 10. 面试常考问题
【全埋点方案系列】AppClick全埋点之透明层处理

透明层方案是全埋点中最”暴力”但最彻底的一种:在所有界面顶层叠加一个透明的 View(Overlay),由它先捕获所有触摸事件,识别出被点击的真实目标 View 并完成埋点,再将事件透传给下层。

一、AOSP 事件分发机制回顾

1.1 ViewGroup.dispatchTouchEvent 源码分析

frameworks/base/core/java/android/view/ViewGroup.java 中,事件分发遵循以下规则:

// ViewGroup.java (AOSP) - 简化版
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
final int actionMasked = ev.getActionMasked();

// Step 1: 是否拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}

// Step 2: 如果没有拦截,遍历子 View 分发
if (!intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = children[i];
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(...)) {
continue;
}
// 递归分发给子 View
if (child.dispatchTouchEvent(ev)) {
mFirstTouchTarget = child;
handled = true;
break;
}
}
}

// Step 3: 如果没有子 View 消费事件,自己处理
if (mFirstTouchTarget == null) {
handled = super.dispatchTouchEvent(ev); // → View.dispatchTouchEvent → onTouchEvent
}
return handled;
}

透明层方案的关键:如果透明层 View 在 DecorView 的子 View 列表的最末尾(即最顶层),它的 dispatchTouchEvent 会最先被调用。

1.2 事件穿透的条件

要让透明层捕获事件但不消费它,需要满足两个条件:

  1. **onTouchEvent() 返回 false**:表示不消费事件。
  2. **isClickable = falseisFocusable = false**:防止 ViewGroup 的分发流程在此中断(否则 mFirstTouchTarget 会被设为透明层,后续子 View 收不到事件)。

二、完整实现

2.1 TrackingOverlay 实现

/**
* 全埋点透明覆盖层
*
* 设计原则:
* 1. 完全透明(alpha = 0,不设置背景)
* 2. 不拦截点击(isClickable = false)
* 3. 返回 false(不消费事件,使之穿透到下层)
* 4. 在 ACTION_UP 时通过坐标识别目标 View 并埋点
*
* @param context Context
* @param config 埋点配置
*/
class TrackingOverlay(
context: Context,
private val config: OverlayConfig = OverlayConfig()
) : View(context) {

// ========== 状态变量 ==========

private val touchSlop by lazy { ViewConfiguration.get(context).scaledTouchSlop }
private var downX = 0f
private var downY = 0f
private var downRawX = 0f
private var downRawY = 0f
private var downTime = 0L
private var gestureState = GestureState.UNKNOWN

// 坐标查找缓存(同一次触摸期间复用)
private var cachedTargetView: WeakReference<View>? = null

// ========== 点击处理器 ==========

private var onViewClicked: ((View, MotionEvent) -> Unit)? = null

init {
// 关键设置:确保透明层不干扰事件分发
isClickable = false
isFocusable = false
isFocusableInTouchMode = false
alpha = 0f
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO

// 可选:对开发者工具显示此 View 的名称
if (BuildConfig.DEBUG) {
alpha = 0.02f // 开发者眼中微可见
setTag("TrackingOverlay")
}
}

override fun onTouchEvent(event: MotionEvent): Boolean {
handleTouchEvent(event)
// 关键:始终返回 false,让事件穿透
return false
}

private fun handleTouchEvent(event: MotionEvent) {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> handleDown(event)
MotionEvent.ACTION_MOVE -> handleMove(event)
MotionEvent.ACTION_UP -> handleUp(event)
MotionEvent.ACTION_CANCEL -> handleCancel()
}
}

private fun handleDown(event: MotionEvent) {
downX = event.x
downY = event.y
downRawX = event.rawX
downRawY = event.rawY
downTime = SystemClock.elapsedRealtime()
gestureState = GestureState.UNKNOWN
cachedTargetView = null
}

private fun handleMove(event: MotionEvent) {
if (gestureState == GestureState.UNKNOWN) {
val dx = event.rawX - downRawX
val dy = event.rawY - downRawY
if (sqrt(dx * dx + dy * dy) > touchSlop) {
gestureState = GestureState.SCROLL
}
}
}

private fun handleUp(event: MotionEvent) {
if (gestureState != GestureState.UNKNOWN) return

val elapsed = SystemClock.elapsedRealtime() - downTime

if (elapsed >= ViewConfiguration.getLongPressTimeout()) {
gestureState = GestureState.LONG_PRESS
return
}

gestureState = GestureState.TAP

// 查找被点击的目标 View
val targetView = findTargetView(event)
if (targetView != null && config.shouldTrack(targetView)) {
onViewClicked?.invoke(targetView, event)
}
}

private fun handleCancel() {
gestureState = GestureState.CANCELLED
cachedTargetView = null
}

// ========== 目标 View 查找 ==========

/**
* 通过屏幕坐标查找被点击的最深层 View
* 从 DecorView 开始遍历整个 View 树
*/
private fun findTargetView(event: MotionEvent): View? {
// 尝试使用缓存
cachedTargetView?.get()?.let { return it }

val decorView = (parent as? View)?.rootView as? ViewGroup ?: return null
val result = findDeepestView(decorView, event.rawX.toInt(), event.rawY.toInt())

// 排除透明层自身
if (result == this) return null

cachedTargetView = result?.let { WeakReference(it) }
return result
}

/**
* 深度优先查找坐标命中的最深层 View
*
* 算法要点:
* 1. 从后向前遍历(后添加的 View Z-order 更高)
* 2. 过滤不可见 View
* 3. 检查坐标是否在 View 边界内
* 4. 如果是 ViewGroup,递归查找更深层的子 View
*/
private fun findDeepestView(parent: ViewGroup, rawX: Int, rawY: Int): View? {
// 从顶层到底层遍历
for (i in parent.childCount - 1 downTo 0) {
val child = parent.getChildAt(i) ?: continue

// 过滤透明层自身
if (child == this) continue

// 过滤不可见 View
if (child.visibility != View.VISIBLE) continue
if (child.alpha == 0f && child !is ViewGroup) continue

// 获取 View 在屏幕上的位置
val loc = IntArray(2)
child.getLocationOnScreen(loc)

val left = loc[0]
val top = loc[1]
val right = left + child.width
val bottom = top + child.height

// 坐标判断
if (rawX in left..right && rawY in top..bottom) {
// 命中,继续查找更深层的子 View
if (child is ViewGroup && child.childCount > 0) {
return findDeepestView(child, rawX, rawY) ?: child
}
return child
}
}

// 所有子 View 都未命中,返回父 View 自身
return parent
}

// ========== 配置方法 ==========

fun setOnViewClickedListener(listener: (View, MotionEvent) -> Unit) {
this.onViewClicked = listener
}

fun updateConfig(newConfig: OverlayConfig) {
// config 不可变,这里仅展示更新入口
}

// ========== 手势状态枚举 ==========

private enum class GestureState {
UNKNOWN, TAP, SCROLL, LONG_PRESS, CANCELLED
}
}

2.2 OverlayConfig 配置

/**
* 透明层配置
*/
data class OverlayConfig(
/** 需要排除的 View ID(如密码输入框) */
val excludeViewIds: Set<Int> = emptySet(),
/** 需要排除的 View 类型 */
val excludeViewTypes: Set<String> = setOf("WebView", "TextureView", "SurfaceView"),
/** 是否采集 View 文本内容 */
val collectText: Boolean = true,
/** 文本采集最大长度 */
val textMaxLength: Int = 100,
/** 坐标查找缓存大小 */
val coordinateCacheSize: Int = 20,
/** 是否启用长按追踪 */
val trackLongPress: Boolean = false,
/** 是否启用手势追踪 */
val trackGestures: Boolean = false,
/** 滑动事件上报间隔(ms),0 表示不上报 */
val scrollReportIntervalMs: Long = 0
) {
fun shouldTrack(view: View): Boolean {
return when {
view.id in excludeViewIds -> false
view.javaClass.simpleName in excludeViewTypes -> false
else -> true
}
}
}

2.3 信息采集器

/**
* 透明层方案的点击信息采集器
* 从 View + MotionEvent 中提取标准化的埋点信息
*/
class OverlayClickCollector {

fun collect(targetView: View, event: MotionEvent): Map<String, Any?> {
return buildMap {
// === View 属性 ===
put("view_class", targetView.javaClass.name)
put("view_simple_name", targetView.javaClass.simpleName)
put("view_id", targetView.id)
put("view_id_name", resolveResourceName(targetView))

// === 文本内容 ===
put("view_text", extractText(targetView))
put("content_description", (targetView.contentDescription?.toString() ?: "").take(100))

// === 坐标信息 ===
put("click_x", event.rawX)
put("click_y", event.rawY)
put("view_width", targetView.width)
put("view_height", targetView.height)

// 计算点击位置在 View 内部的相对坐标
val viewLoc = IntArray(2)
targetView.getLocationOnScreen(viewLoc)
put("click_x_in_view", event.rawX - viewLoc[0])
put("click_y_in_view", event.rawY - viewLoc[1])

// === View 层级路径 ===
put("view_path", buildViewPath(targetView))

// === 时间 ===
put("event_time", event.eventTime)
put("click_timestamp", System.currentTimeMillis())

// === 页面上下文 ===
val activity = getActivity(targetView.context)
put("page_name", activity?.javaClass?.simpleName ?: "unknown")
put("page_class", activity?.javaClass?.name ?: "")
put("page_title", activity?.title?.toString() ?: "")

// === 额外标记 ===
put("tracking_method", "overlay") // 标记数据来源
}
}

private fun resolveResourceName(view: View): String {
return try {
if (view.id != View.NO_ID) view.resources.getResourceEntryName(view.id) else ""
} catch (_: Resources.NotFoundException) { "" }
}

private fun extractText(view: View): String {
return when (view) {
is EditText -> "" // 隐私保护,不采集输入框内容
is TextView -> view.text?.toString()?.take(100) ?: ""
else -> ""
}
}

private fun buildViewPath(view: View): String {
val parts = mutableListOf<String>()
var current: View? = view
while (current != null && current !is DecorView) {
val segment = "${current.javaClass.simpleName}${
if (current.id != View.NO_ID) "[${resolveResourceName(current)}]" else ""
}"
parts.add(0, segment)
current = (current.parent as? View)
}
return parts.joinToString("/")
}

private fun getActivity(context: Context): Activity? {
var ctx = context
while (ctx is ContextWrapper) {
if (ctx is Activity) return ctx
ctx = ctx.baseContext
}
return null
}
}

三、全局注入方案

3.1 自动添加透明层

/**
* 全局透明层注入器
*
* 在 Activity 创建时,向 DecorView 添加透明层
* 使用 ActivityLifecycleCallbacks 实现自动注入
*/
class OverlayLifecycleInjector : Application.ActivityLifecycleCallbacks {

private val collector = OverlayClickCollector()
private val deduplicator = OverlayClickDeduplicator()
private val config = OverlayConfig()

// Activity → Overlay 映射
private val overlays = WeakHashMap<Activity, TrackingOverlay>()

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
addOverlayToActivity(activity)
}

override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
overlays.remove(activity)
}

private fun addOverlayToActivity(activity: Activity) {
val decorView = activity.window.decorView as? ViewGroup ?: return

// 防止重复添加
if (overlays.containsKey(activity)) return

val overlay = TrackingOverlay(activity, config).apply {
setOnViewClickedListener { view, event ->
handleViewClicked(view, event)
}
}

// 确保 DecorView 的布局已完成
decorView.post {
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
decorView.addView(overlay, params)
// 确保透明层在最顶层
overlay.bringToFront()
}

overlays[activity] = overlay
}

private fun handleViewClicked(view: View, event: MotionEvent) {
if (!deduplicator.shouldTrack(view, event)) return

val info = collector.collect(view, event)

// 异步上报
TrackerExecutor.execute {
AnalyticsSDK.track("app_click", info)
}
}
}

/**
* 透明层专用去重器
*/
class OverlayClickDeduplicator {
private val recentClicks = LruCache<Long, Boolean>(maxSize = 100)

fun shouldTrack(view: View, event: MotionEvent): Boolean {
// 使用 eventTime + viewHash 做去重 key
val key = ((event.eventTime / 100) * 31 + view.hashCode())
if (recentClicks.get(key) == true) return false
recentClicks.put(key, true)
return true
}
}

3.2 Application 注册

class TrackingApplication : Application() {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(OverlayLifecycleInjector())
}
}

四、架构流程图

┌──────────────────────────────────────────────────────┐
│ DecorView │
│ ┌────────────────────────────────────────────────┐ │
│ │ StatusBar / ActionBar / Content │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ 业务布局层 (XML / 代码创建的 View) │ │ │
│ │ │ Button TextView RecyclerView ... │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────┐ │
│ │ TrackingOverlay (透明覆盖层) ← 最顶层 │ │
│ │ - alpha = 0 │ │
│ │ - isClickable = false │ │
│ │ - onTouchEvent → 识别目标View → return false │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘

触摸事件流:
Finger Touch (x, y)


DecorView.dispatchTouchEvent()


遍历子 View (Z-order 从高到低)


┌─────────────────────────────┐
│ TrackingOverlay │ ← 最先收到事件(最顶层)
│ dispatchTouchEvent → false │
└─────────────────────────────┘
│ (未被消费,继续向下)

遍历下一个子 View

├── StatusBar/TitleBar (如果存在)


┌─────────────────────────────┐
│ 业务 View (Button, etc.) │ ← 最终消费者
│ dispatchTouchEvent → true │
└─────────────────────────────┘

异步处理:
TrackingOverlay.ACTION_UP
→ findDeepestView() (通过坐标查找目标View)
→ OverlayClickCollector (提取View信息)
→ OverlayClickDeduplicator(去重)
→ AnalyticsSDK.track() (异步上报)

五、高级场景处理

5.1 RecyclerView 快速滚动优化

/**
* RecyclerView 滚动感知的透明层
* 在快速滚动时暂停坐标查找
*/
class RecyclerViewAwareOverlay(context: Context) : TrackingOverlay(context) {

private var isRecyclerViewScrolling = false
private val scrollEndRunnable = Runnable { isRecyclerViewScrolling = false }

fun bindToRecyclerView(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
isRecyclerViewScrolling = newState != RecyclerView.SCROLL_STATE_IDLE
if (newState == RecyclerView.SCROLL_STATE_SETTLING ||
newState == RecyclerView.SCROLL_STATE_DRAGGING) {
// 滚动中,移除延迟恢复
handler.removeCallbacks(scrollEndRunnable)
} else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// 滚动停止,延迟 150ms 恢复追踪(等待可能的惯性点击)
handler.postDelayed(scrollEndRunnable, 150)
}
}
})
}

override fun onTouchEvent(event: MotionEvent): Boolean {
if (isRecyclerViewScrolling && event.actionMasked == MotionEvent.ACTION_UP) {
return false // 跳过滑动中的 UP 事件
}
return super.onTouchEvent(event)
}
}

5.2 Dialog / PopupWindow 覆盖

/**
* Dialog 透明层注入工具
*
* Dialog 使用独立的 Window,DecorView 不在 Activity 的树内
*/
object DialogOverlayInjector {

private val injectedDialogs = WeakHashMap<Dialog, TrackingOverlay>()

fun inject(dialog: Dialog) {
dialog.setOnShowListener {
val decorView = dialog.window?.decorView as? ViewGroup ?: return@setOnShowListener
if (injectedDialogs.containsKey(dialog)) return@setOnShowListener

val overlay = TrackingOverlay(dialog.context)
overlay.setOnViewClickedListener { view, event ->
// 埋点处理...
}

val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
decorView.addView(overlay, params)
injectedDialogs[dialog] = overlay
}
}
}

// 扩展函数
fun Dialog.enableOverlayTracking() = DialogOverlayInjector.inject(this)

5.3 分屏 / 多窗口模式适配

/**
* 分屏模式适配
*
* 问题:分屏时 DecorView 坐标发生变化,绝对坐标失效
* 解决:使用相对于 DecorView 的坐标进行计算
*/
fun TrackingOverlay.prepareForMultiWindow(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.addOnMultiWindowModeChangedListener { isInMultiWindowMode ->
if (isInMultiWindowMode) {
// 重设透明层布局参数
val decorView = parent as? ViewGroup ?: return@addOnMultiWindowModeChangedListener
layoutParams?.apply {
width = decorView.width
height = decorView.height
}
requestLayout()
}
}
}
}

六、性能优化

6.1 坐标查找优化

/**
* 坐标查找性能对比:
*
* 场景:200 个 View 的页面
*
* 策略 | 查找耗时 | 内存 | 准确度
* ------------------|----------|------|-------
* 全量遍历 (每帧) | ~1.5ms | 低 | 100%
* 全量遍历 (仅UP时) | ~0ms/帧 | 低 | 100%
* 缓存 (坐标分桶) | ~0.1ms | 中 | 95%↑
* ACTION_DOWN预查找 | ~0ms/帧 | 低 | 90%↑
*
* 推荐:ACTION_DOWN 预查找 + 坐标分桶缓存
*/
class OptimizedViewFinder(
private val decorView: ViewGroup
) {
// 坐标分桶缓存:key = "x_bucket/y_bucket" → View
private val cache = LruCache<String, WeakReference<View>>(maxSize = 100)

fun findView(rawX: Int, rawY: Int): View? {
val bucketKey = "${rawX / 20}_${rawY / 20}"
cache.get(bucketKey)?.get()?.let { return it }

val view = findDeepestView(decorView, rawX, rawY)
view?.let { cache.put(bucketKey, WeakReference(it)) }
return view
}
}

七、与手势导航栏的冲突处理

Android 10+ 的手势导航栏会在屏幕边缘添加系统手势区域,透明层可能与之冲突。

/**
* 处理与系统手势导航的冲突
* Android 10+ 的边缘返回手势可能与透明层重叠
*/
class GestureAwareOverlay(context: Context) : TrackingOverlay(context) {

private val gestureInsets: WindowInsets? = null

override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets {
val systemGestures = insets?.getInsets(WindowInsets.Type.systemGestures())
// 设置 padding 避开系统手势区域
setPadding(
systemGestures?.left ?: 0,
systemGestures?.top ?: 0,
systemGestures?.right ?: 0,
systemGestures?.bottom ?: 0
)
return super.onApplyWindowInsets(insets)
}

override fun onTouchEvent(event: MotionEvent): Boolean {
// 检查触摸点是否在系统手势区域内
if (isInSystemGestureArea(event)) {
return false // 系统手势区域,不处理
}
return super.onTouchEvent(event)
}

private fun isInSystemGestureArea(event: MotionEvent): Boolean {
// 检查是否在 padding 区域内(即系统手势区域)
return event.x < paddingLeft ||
event.x > width - paddingRight ||
event.y < paddingTop ||
event.y > height - paddingBottom
}
}

八、与其他方案组合

透明层方案常与其他方案组合使用,形成多层防御的埋点体系:

层级 方案 覆盖场景
第一层 透明层 (Overlay) 所有触摸事件的兜底捕获
第二层 Window.Callback 代理 精确的事件分发时序控制
第三层 OnClickListener 代理 最详细的 View 属性采集
补充层 AccessibilityDelegate 无障碍场景的额外覆盖

九、ProGuard/R8 规则

-keep class com.example.tracking.overlay.TrackingOverlay { *; }
-keep class com.example.tracking.overlay.OverlayConfig { *; }
-keep class com.example.tracking.overlay.OverlayClickCollector { *; }
-keep class com.example.tracking.overlay.OverlayLifecycleInjector { *; }

面试常考问题

Q1:透明层方案是否会阻挡底层 View 的点击响应?

不会。透明层重写 onTouchEvent 返回 false,表示不消费事件,系统会继续将事件传递给下层 View。同时 isClickable = false 确保了触摸事件在 ViewGroup.dispatchTouchEvent 的分发阶段不会在此层停止(不会将 mFirstTouchTarget 设为本层)。关键在于理解 ViewGroup 的分发逻辑:onInterceptTouchEvent 返回 false + onTouchEvent 返回 false,事件会继续分发给下一个兄弟 View。

Q2:透明层能否识别被裁剪或半透明覆盖的 View?

只能识别透明层下方”坐标范围内”的最深层可见 View。坐标查找基于 View.getLocationOnScreen()View.isShown(),按 Z-order 从高到低遍历。如果目标 View 的上方有一个半透明遮罩(比如一个 alpha=0.5 的 Dialog 背景),坐标查找会命中上层的遮罩 View 而非目标 View,这是坐标查找的固有局限。解决途径:(1)检查命中 View 的 alpha 和 background 属性,对半透明 View 继续向下查找;(2)在埋点信息中标记命中的 View 是否是预期的交互控件(通过检查 isClickable / hasOnClickListeners())。

Q3:如何处理 RecyclerView 快速滚动时的性能问题?

透明层添加触摸事件阈值过滤(如位移 < touchSlop 才判定为点击),避免在滚动过程中频繁执行坐标遍历。优化层级:(1)在 ACTION_DOWN 时不做全量查找(仅记录坐标);(2)在 ACTION_MOVE 时检测是否超过 touchSlop,超过则标记为滚动,后续 MOVE/UP 不再查找;(3)将坐标查找延后到 ACTION_UP 时一次性执行;(4)绑定 RecyclerView 的 OnScrollListener,在 SCROLL_STATE_SETTLINGSCROLL_STATE_DRAGGING 期间完全禁用查找;(5)使用坐标分桶缓存提高重复坐标的查找效率。AOSP 事件分发流程参见 ViewGroup.dispatchTouchEvent() 中的 onInterceptTouchEvent() 和子 View 遍历逻辑(frameworks/base/core/java/android/view/ViewGroup.java)。

Q4:透明层方案在 Jetpack Compose 中能工作吗?

不能直接工作。Compose 使用自己的渲染和事件系统,不走 Android ViewGroup 的 dispatchTouchEvent。Compose 的 UI 树由 AndroidComposeView(一个 ViewGroup)承载,所有 Compose 组件都在它的 Canvas 上绘制。透明层方案只能看到 AndroidComposeView 这一层,无法区分其内部的不同 Composable 函数。Compose 的全埋点需要专门的方案:(1)在 Compose 的 Modifier.clickable { } 中插入埋点;(2)使用 Compose 的 CompositionLocals 传递埋点上下文;(3)在 Compose 的 Semantics 树中注入追踪信息。

Q5:透明层方案与 GrowingIO 的实现有什么关系?

GrowingIO 是 Android 全埋点领域中最早使用透明层方案的公司之一。GrowingIO 的 Android SDK 在 ViewTreeObserver.OnGlobalLayoutListener 中为每个 Activity 动态添加透明层,通过坐标查找实现无埋点点击追踪。其核心实现原理与本文描述一致,但在工程化方面做了更多工作:(1)多窗口支持(Dialog/PopupWindow/Toast);(2)Hybrid 支持(WebView 内的事件通过 JS Bridge 上报);(3)圈选功能(可视化埋点配置,使透明层在圈选模式下变为半透明可视化,显示每个 View 的可追踪信息)。与之相比,神策数据的 Android SDK 使用的是 Window.Callback 代理方案。

打赏
  • 微信
  • 支付宝

评论