目录
  1. 1. 一、核心原理
  2. 2. 二、实现代码
  3. 3. 三、在 Application 中全局注入
  4. 4. 四、与 OnClickListener 方案对比
  5. 5. 面试常考问题
【全埋点方案系列】AppClick全埋点之Window.Callback代理

Window.Callback 代理方案是在更底层——Window 级别拦截所有触摸事件,从中识别点击行为并完成埋点。相比 OnClickListener 代理,它覆盖了所有 View(包括动态创建和无 Listener 的 View),且不依赖反射操作私有字段。

一、核心原理

每个 Activity/Dialog 都持有一个 Window,Window 内部有一个 Window.Callback 接口,负责分发按键、触摸、焦点等事件。ActivityDialog 都实现了该接口。Android 中的触摸事件链路:

DecorView.dispatchTouchEvent()
→ Window.Callback.dispatchTouchEvent()
→ Activity.dispatchTouchEvent()
→ PhoneWindow.superDispatchTouchEvent()
→ DecorView.superDispatchTouchEvent()
→ ViewGroup.dispatchTouchEvent()

二、实现代码

class TrackingWindowCallback(
private val origin: Window.Callback
) : Window.Callback by origin {

override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
event ?: return origin.dispatchTouchEvent(event)

if (event.action == MotionEvent.ACTION_UP) {
handleClickEvent(event)
}

return origin.dispatchTouchEvent(event)
}

private fun handleClickEvent(event: MotionEvent) {
// 从 DecorView 开始查找被点击的目标 View
val decorView = (origin as? Activity)?.window?.decorView ?: return
val targetView = findTargetView(decorView, event.rawX.toInt(), event.rawY.toInt())
targetView?.let { track(it) }
}

private fun findTargetView(parent: ViewGroup, x: Int, y: Int): View? {
for (i in parent.childCount - 1 downTo 0) {
val child = parent.getChildAt(i)
val loc = IntArray(2)
child.getLocationOnScreen(loc)
if (x in loc[0]..(loc[0] + child.width) &&
y in loc[1]..(loc[1] + child.height)) {
if (child is ViewGroup) return findTargetView(child, x, y) ?: child
return child
}
}
return parent
}

private fun track(view: View) {
AnalyticsSDK.track("app_click", mapOf(
"view_id" to view.id,
"class" to view.javaClass.simpleName,
"text" to ((view as? TextView)?.text?.toString() ?: ""),
"resource_name" to view.resources.getResourceEntryName(view.id)
))
}
}

三、在 Application 中全局注入

class TrackingApplication : Application() {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
// 把 Activity 自身的 Window.Callback 替换为代理
val origin = activity.window.callback
activity.window.callback = TrackingWindowCallback(origin)
}
// ... 其他回调空实现
})
}
}

四、与 OnClickListener 方案对比

维度 OnClickListener 代理 Window.Callback 代理
覆盖度 仅覆盖有 Listener 的 View 覆盖所有 View 点击
实现复杂度 简单(需反射) 中等(需坐标计算)
性能开销 极低 坐标遍历有开销
扩展性 仅点击 可扩展手势/长按
多窗口支持 需单独处理 Window 级别天然支持

面试常考问题

Q1:Window.Callback 的代理委托模式(by)有什么优势?

Kotlin 的 by 委托让代理类无需手动实现 Callback 的所有方法,未覆盖的方法自动转发给原始 Callback,代码量极简,只重写需要拦截的 dispatchTouchEvent

Q2:如何区分 click 和 scroll?

通过 event.action 判断:记录 ACTION_DOWN 位置,ACTION_UP 时计算位移距离。若 sqrt(dx^2 + dy^2) < touchSlop(系统默认 8dp),判定为点击;否则为滑动。Android 的 ViewConfiguration.getScaledTouchSlop() 获取系统阈值。

Q3:Dialog/PopupWindow 如何覆盖?

Dialog 有自己的 Window,需要单独 Hook。可通过 Dialog.setContentView() 时替换其 Window.Callback,或在 Application 级别 Hook Dialog 的创建。PopupWindow 使用 WindowManager.addView() 添加窗口,需通过 WindowManagerGlobal 的代理来拦截。AOSP 源码路径:Dialog.javamWindow 属性和 Window.Callback 接口定义于 frameworks/base/core/java/android/view/Window.java

打赏
  • 微信
  • 支付宝

评论