目录
  1. 1. 一、AOSP 源码视角:Window.Callback 的完整协议
    1. 1.1. 1.1 Window.Callback 接口定义
    2. 1.2. 1.2 Activity 与 Window.Callback 的关系
    3. 1.3. 1.3 dispatchTouchEvent 的完整调用链
    4. 1.4. 1.4 Kotlin 委托模式(by 关键字)的优势
  2. 2. 二、完整的代理实现
    1. 2.1. 2.1 核心代理类
    2. 2.2. 2.2 埋点信息采集器(Window.Callback 版本)
    3. 2.3. 2.3 埋点事件处理器
  3. 3. 三、全局注入方案
    1. 3.1. 3.1 自动注入 Activity
    2. 3.2. 3.2 Dialog 注入
    3. 3.3. 3.3 PopupWindow 注入
  4. 4. 四、架构流程图
  5. 5. 五、与 OnClickListener 代理方案对比
  6. 6. 六、高级场景
    1. 6.1. 6.1 RecyclerView 中的精准点击判断
    2. 6.2. 6.2 与 GestureDetector 共存
    3. 6.3. 6.3 性能优化:坐标查找的可选策略
  7. 7. 七、集成主流分析 SDK
    1. 7.1. 7.1 神策 Sensors Analytics
    2. 7.2. 7.2 GrowingIO
  8. 8. 八、性能指标与影响分析
    1. 8.1. 8.1 坐标遍历的性能开销
    2. 8.2. 8.2 内存占用
  9. 9. 九、ProGuard/R8 规则
  10. 10. 面试常考问题
【全埋点方案系列】AppClick全埋点之Window.Callback代理

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

一、AOSP 源码视角:Window.Callback 的完整协议

1.1 Window.Callback 接口定义

frameworks/base/core/java/android/view/Window.java 中,Window.Callback 接口定义了以下方法:

// Window.java (AOSP)
public interface Callback {
// 按键事件
public boolean dispatchKeyEvent(KeyEvent event);
// 快捷键事件(Android 11+)
public boolean dispatchKeyShortcutEvent(KeyEvent event);
// 触摸事件 —— 这是全埋点的核心切入点
public boolean dispatchTouchEvent(MotionEvent event);
// 轨迹球事件
public boolean dispatchTrackballEvent(MotionEvent event);
// 通用运动事件(游戏手柄等)
public boolean dispatchGenericMotionEvent(MotionEvent event);
// 窗口焦点变化
public void onWindowFocusChanged(boolean hasFocus);
// 附着到 Window / 从 Window 分离
public void onAttachedToWindow();
public void onDetachedFromWindow();
// 面板(选项菜单、上下文菜单)
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
public View onCreatePanelView(int featureId);
public boolean onCreatePanelMenu(int featureId, Menu menu);
public boolean onPreparePanel(int featureId, View view, Menu menu);
public boolean onMenuOpened(int featureId, Menu menu);
public boolean onMenuItemSelected(int featureId, MenuItem item);
// ActionMode(上下文操作栏)
public ActionMode onWindowStartingActionMode(ActionMode.Callback callback);
public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type);
// 搜索
public boolean onSearchRequested();
public boolean onSearchRequested(SearchEvent searchEvent);
// ActionBar
public void onActionModeStarted(ActionMode mode);
public void onActionModeFinished(ActionMode mode);
// 内容变化
public void onContentChanged();
// 窗口属性变化
public void onWindowAttributesChanged(WindowManager.LayoutParams attrs);
}

1.2 Activity 与 Window.Callback 的关系

Activity 实现了 Window.Callback 接口。在 Activity.attach() 方法中:

// Activity.java (AOSP)
final void attach(Context context, ActivityThread aThread, ...) {
// ...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setCallback(this); // 将 Activity 自身设置为 Window 的回调
// ...
}

1.3 dispatchTouchEvent 的完整调用链

InputReaderThread (native)
→ InputDispatcher (native)
→ ViewRootImpl$WindowInputEventReceiver.onInputEvent(InputEvent)
→ ViewRootImpl.enqueueInputEvent(QueuedInputEvent)
→ ViewRootImpl.doProcessInputEvents()
→ ViewRootImpl.deliverInputEvent(QueuedInputEvent)
→ 根据事件类型分派
→ ViewRootImpl.dispatchPointerEvent(MotionEvent)
→ DecorView.dispatchPointerEvent(MotionEvent)
→ 检查 mWindow.Callback
【关键拦截点】
→ mWindow.Callback.dispatchTouchEvent(event) ← 我们在这里拦截
→ Activity.dispatchTouchEvent(event)
→ getWindow().superDispatchTouchEvent(event)
→ PhoneWindow.superDispatchTouchEvent(event)
→ mDecor.superDispatchTouchEvent(event)
→ DecorView.superDispatchTouchEvent(event)
→ super.dispatchTouchEvent(event) // ViewGroup
→ ViewGroup.dispatchTouchEvent()
→ 遍历子 View,分派事件

拦截时机分析:我们在 mWindow.Callback.dispatchTouchEvent() 这一层拦截,此时可以获取原始的 MotionEvent,但 View 树的分发尚未开始。这意味着我们有两种点击识别策略:

  1. 事件层面识别:仅根据 MotionEvent 的坐标和时序判断是否为点击(无需查找目标 View)。
  2. 目标 View 查找:在 ACTION_UP 时通过坐标遍历 DecorView 找到目标 View,获取其属性信息。

1.4 Kotlin 委托模式(by 关键字)的优势

使用 Kotlin 的类委托(Class Delegation)可以极大简化代理类的实现:

// 所有未重写的方法自动转发给 origin
class TrackingWindowCallback(
private val origin: Window.Callback
) : Window.Callback by origin {
// 只需重写我们需要拦截的方法
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
// ...
return origin.dispatchTouchEvent(event)
}
}

如果你使用 Java,需要手动实现所有 20+ 个接口方法并逐个转发,代码量膨胀严重。Kotlin 的 by 委托是 Window.Callback 代理方案简洁性的重要保障。

二、完整的代理实现

2.1 核心代理类

/**
* Window.Callback 全埋点代理
*
* 使用 Kotlin 委托模式,只需重写需要拦截的方法
* 在 dispatchTouchEvent 中识别点击行为
*/
class TrackingWindowCallback(
private val origin: Window.Callback
) : Window.Callback by origin {

// ========== 点击识别状态机 ==========

// 记录 ACTION_DOWN 的位置和时间
private var downRawX = Float.NaN
private var downRawY = Float.NaN
private var downTime = 0L
private var downView: WeakReference<View>? = null

// 触摸 Slop 阈值(来自 ViewConfiguration)
private var touchSlop = 0

// 长按检测相关
private var mayBeLongClick = false
private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong() // 默认 500ms

// 手势类型最终判定
private var gestureType: GestureType = GestureType.UNKNOWN

// ========== 回调接口 ==========

private var clickHandler: ((View, MotionEvent) -> Unit)? = null
private var longClickHandler: ((View, MotionEvent) -> Unit)? = null
private var scrollHandler: ((MotionEvent, Float, Float) -> Unit)? = null

init {
// 延迟获取 touchSlop,因为需要 Context
// 实际项目中通过 origin 获取 Activity 再获取 touchSlop
}

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

// 在分发前处理
handleTouchEventBeforeDispatch(event)

// 转发给原始 Callback(Activity)
val handled = origin.dispatchTouchEvent(event)

// 在分发后处理
handleTouchEventAfterDispatch(event, handled)

return handled
}

/**
* 事件分发前处理:记录状态,识别手势
*/
private fun handleTouchEventBeforeDispatch(event: MotionEvent) {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
downRawX = event.rawX
downRawY = event.rawY
downTime = SystemClock.elapsedRealtime()
mayBeLongClick = true
gestureType = GestureType.UNKNOWN

// 预查找目标 View,用于后续信息采集
downView = findTargetView(event)?.let { WeakReference(it) }
}

MotionEvent.ACTION_MOVE -> {
if (gestureType == GestureType.UNKNOWN) {
val dx = event.rawX - downRawX
val dy = event.rawY - downRawY
val distance = sqrt(dx * dx + dy * dy)

if (distance > getTouchSlop(event)) {
gestureType = GestureType.SCROLL
mayBeLongClick = false

// 滚动开始回调
scrollHandler?.invoke(event, dx, dy)
}
}
}

MotionEvent.ACTION_POINTER_DOWN -> {
// 多指触摸,取消可能的单击判定
mayBeLongClick = false
gestureType = GestureType.MULTI_TOUCH
}

MotionEvent.ACTION_CANCEL -> {
resetState()
}
}
}

/**
* 事件分发后处理:ACTION_UP 时最终判定
*/
private fun handleTouchEventAfterDispatch(event: MotionEvent, handled: Boolean) {
when (event.actionMasked) {
MotionEvent.ACTION_UP -> {
val elapsed = SystemClock.elapsedRealtime() - downTime

if (gestureType == GestureType.UNKNOWN) {
// 在 UP 之前没有移动,判定为点击
gestureType = if (elapsed >= longPressTimeout) {
GestureType.LONG_CLICK
} else {
GestureType.CLICK
}
}

when (gestureType) {
GestureType.CLICK -> {
val targetView = downView?.get() ?: findTargetView(event)
if (targetView != null) {
clickHandler?.invoke(targetView, event)
}
}
GestureType.LONG_CLICK -> {
val targetView = downView?.get()
if (targetView != null) {
longClickHandler?.invoke(targetView, event)
}
}
GestureType.SCROLL -> {
// 滚动不需要处理
}
else -> {}
}

resetState()
}
}
}

/**
* 通过坐标查找目标 View
* 使用深度优先遍历,找到最后一个包含该坐标的最深层 View
*/
private fun findTargetView(event: MotionEvent): View? {
val decorView = getDecorView() ?: return null
return findDeepestView(decorView, event.rawX.toInt(), event.rawY.toInt())
}

private fun findDeepestView(parent: ViewGroup, x: Int, y: Int): View? {
// 从后向前遍历(后添加的 View 绘制在上层)
for (i in parent.childCount - 1 downTo 0) {
val child = parent.getChildAt(i) ?: continue

// 过滤不可见和透明的 View
if (child.visibility != View.VISIBLE) continue
if (child.alpha == 0f) continue

val loc = IntArray(2)
child.getLocationOnScreen(loc)

val childLeft = loc[0]
val childTop = loc[1]
val childRight = childLeft + child.width
val childBottom = childTop + child.height

if (x in childLeft..childRight && y in childTop..childBottom) {
if (child is ViewGroup) {
// 递归查找更深层的子 View
return findDeepestView(child, x, y) ?: child
}
return child
}
}
return parent
}

// ========== 辅助方法 ==========

private fun getDecorView(): ViewGroup? {
return (origin as? Activity)?.window?.decorView as? ViewGroup
?: (origin as? Dialog)?.window?.decorView as? ViewGroup
}

private fun getTouchSlop(event: MotionEvent): Float {
if (touchSlop == 0) {
touchSlop = ViewConfiguration.get(
(origin as? Activity) ?: return 24f
).scaledTouchSlop
}
return touchSlop.toFloat()
}

private fun resetState() {
downRawX = Float.NaN
downRawY = Float.NaN
downTime = 0L
downView = null
mayBeLongClick = false
gestureType = GestureType.UNKNOWN
}

// ========== 公开配置方法 ==========

fun setOnClickHandler(handler: (View, MotionEvent) -> Unit) {
this.clickHandler = handler
}

fun setOnLongClickHandler(handler: (View, MotionEvent) -> Unit) {
this.longClickHandler = handler
}

fun setOnScrollHandler(handler: (MotionEvent, Float, Float) -> Unit) {
this.scrollHandler = handler
}

// ========== 手势类型枚举 ==========

enum class GestureType {
UNKNOWN, CLICK, LONG_CLICK, SCROLL, MULTI_TOUCH
}
}

2.2 埋点信息采集器(Window.Callback 版本)

/**
* 基于 Window.Callback 的点击信息采集
* 特点:从 MotionEvent + 坐标查找目标 View
*/
class WindowCallbackClickCollector(
private val decorViewSupplier: () -> ViewGroup?
) {

/**
* 采集点击信息
*/
fun collect(view: View, event: MotionEvent): Map<String, Any?> {
return buildMap {
// View 信息
put("view_class", view.javaClass.name)
put("view_simple_name", view.javaClass.simpleName)
put("view_id", view.id)
if (view.id != View.NO_ID) {
try {
put("view_id_name", view.resources.getResourceEntryName(view.id))
} catch (_: Resources.NotFoundException) {
put("view_id_name", "")
}
}

// 文本内容
put("view_text", getViewText(view))

// 坐标信息
put("click_x", event.rawX)
put("click_y", event.rawY)
val decorView = decorViewSupplier() ?: return@buildMap
val decorLoc = IntArray(2)
decorView.getLocationOnScreen(decorLoc)
put("click_x_in_window", event.rawX - decorLoc[0])
put("click_y_in_window", event.rawY - decorLoc[1])

// View 在屏幕上的位置
val viewLoc = IntArray(2)
view.getLocationOnScreen(viewLoc)
put("view_left", viewLoc[0])
put("view_top", viewLoc[1])
put("view_width", view.width)
put("view_height", view.height)

// View 层级路径
put("view_path", buildViewPath(view, decorView))

// 时间戳
put("event_time", event.eventTime)
put("down_time", event.downTime)
put("click_duration_ms", event.eventTime - event.downTime)

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

// 是否在可滚动容器内
put("is_in_scrollable", isInScrollableContainer(view))

// 是否是键盘/手柄输入(通过 source 判断)
put("input_source", event.source)
put("is_touch", event.source == InputDevice.SOURCE_TOUCHSCREEN)
}
}

private fun getViewText(view: View): String {
return when (view) {
is TextView -> view.text?.toString()?.take(100) ?: ""
is ImageView -> view.contentDescription?.toString()?.take(100) ?: ""
else -> view.contentDescription?.toString()?.take(100) ?: ""
}
}

/**
* 构建 View 从根布局开始的层级路径
*/
private fun buildViewPath(view: View, root: ViewGroup): String {
val path = mutableListOf<String>()
var current: View? = view
while (current != null && current != root) {
val segment = buildString {
append(current.javaClass.simpleName)
if (current.id != View.NO_ID) {
try {
append("[${current.resources.getResourceEntryName(current.id)}]")
} catch (_: Exception) {
append("[id:${current.id}]")
}
}
}
path.add(0, segment)
current = (current.parent as? View)
}
return path.joinToString("/")
}

/**
* 检查 View 是否在可滚动容器中(ScrollView, RecyclerView, NestedScrollView 等)
*/
private fun isInScrollableContainer(view: View): Boolean {
var parent = view.parent
while (parent is ViewGroup) {
if (parent is ScrollView ||
parent is NestedScrollView ||
parent is RecyclerView ||
parent is HorizontalScrollView ||
parent is ViewPager2
) {
return true
}
parent = parent.parent
}
return false
}

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

2.3 埋点事件处理器

/**
* 点击埋点的事件处理器
* 负责信息采集、去重、异步上报
*/
class WindowCallbackClickHandler(
private val collector: WindowCallbackClickCollector,
private val deduplicator: ClickDeduplicator = ClickDeduplicator(),
private val analyticsSDK: AnalyticsSDK = AnalyticsSDK.getInstance()
) {

fun handleClick(view: View, event: MotionEvent) {
// 去重检查
if (!deduplicator.shouldTrack(view, event)) return

// 采集信息(主线程)
val info = collector.collect(view, event)

// 异步上报
analyticsSDK.trackAsync("app_click", info)
}

fun handleLongClick(view: View, event: MotionEvent) {
val info = collector.collect(view, event)
analyticsSDK.trackAsync("app_long_click", info)
}
}

/**
* 基于坐标和时间的去重器
* 解决 ACTION_UP 可能被多次触发的问题
*/
class ClickDeduplicator {
private data class ClickKey(
val viewHash: Int,
val xBucket: Int, // 坐标分桶:将像素坐标按 20px 粒度分桶
val yBucket: Int,
val timeBucket: Int // 时间分桶:按 500ms 粒度分桶
)

private val recentClicks = LruCache<ClickKey, Long>(maxSize = 500)
private val windowMs = 1000L

fun shouldTrack(view: View, event: MotionEvent): Boolean {
val now = SystemClock.elapsedRealtime()
val key = ClickKey(
viewHash = view.hashCode(),
xBucket = event.rawX.toInt() / 20,
yBucket = event.rawY.toInt() / 20,
timeBucket = (event.eventTime / 500).toInt()
)

val lastTime = recentClicks.get(key)
if (lastTime != null && (now - lastTime) < windowMs) {
return false // 重复事件
}
recentClicks.put(key, now)
return true
}
}

三、全局注入方案

3.1 自动注入 Activity

class TrackingApplication : Application() {

override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(WindowCallbackInjector())
}
}

/**
* Window.Callback 注入器
* 在 Activity 生命周期中自动替换 Window.Callback
*/
class WindowCallbackInjector : Application.ActivityLifecycleCallbacks {

// 记录已注入的 Window 的 hashCode,避免重复注入
private val injectedWindows = HashSet<Int>()

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
injectToActivity(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) {
injectedWindows.remove(activity.window.hashCode())
}

private fun injectToActivity(activity: Activity) {
val window = activity.window
if (!injectedWindows.add(window.hashCode())) return

val origin = window.callback
if (origin is TrackingWindowCallback) return // 已注入

val trackingCallback = TrackingWindowCallback(origin)
val collector = WindowCallbackClickCollector {
(activity.window.decorView as? ViewGroup)
}
val handler = WindowCallbackClickHandler(collector)

trackingCallback.setOnClickHandler { view, event ->
handler.handleClick(view, event)
}
trackingCallback.setOnLongClickHandler { view, event ->
handler.handleLongClick(view, event)
}

window.callback = trackingCallback
}
}

3.2 Dialog 注入

/**
* Dialog Window.Callback 注入
*
* 问题:Dialog 通常由业务代码动态创建,无法在 Application.onCreate 中统一注入
* 方案:Hook Dialog.setContentView() 或监听 Dialog 的创建
*/
object DialogCallbackInjector {

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

/**
* 在 Dialog.show() 之前调用
*/
fun inject(dialog: Dialog) {
if (injectedDialogs.containsKey(dialog)) return
injectedDialogs[dialog] = true

val window = dialog.window ?: return
val origin = window.callback
if (origin is TrackingWindowCallback) return

val trackingCallback = TrackingWindowCallback(origin)
val collector = WindowCallbackClickCollector {
(dialog.window?.decorView as? ViewGroup)
}
val handler = WindowCallbackClickHandler(collector)

trackingCallback.setOnClickHandler { view, event ->
handler.handleClick(view, event)
}

window.callback = trackingCallback
}
}

// 扩展函数:简化业务方调用
fun Dialog.enableClickTracking() {
DialogCallbackInjector.inject(this)
}

3.3 PopupWindow 注入

/**
* PopupWindow 的 Window.Callback 注入
*
* 与 Activity/Dialog 不同,PopupWindow 使用 WindowManager.addView()
* 直接添加窗口。其内部使用 PopupDecorView。
*
* 方案:反射获取 PopupWindow 的 mWindowManager,设置 Window.Callback
*/
object PopupWindowCallbackInjector {

fun inject(popupWindow: PopupWindow) {
try {
// 反射获取 PopupWindow 内部的 PopupDecorView
val popupField = PopupWindow::class.java.getDeclaredField("mPopup")
popupField.isAccessible = true
val popup = popupField.get(popupWindow)
// PopupWindow$PopupViewContainer 或 PopupWindow$PopupDecorView

val contentView = popupWindow.contentView ?: return
// 对于 PopupWindow,更简单的方式是直接在其 ContentView 上设置 OnTouchListener
// 但由于它不经过 Window.Callback,使用触摸事件代理更合适
} catch (_: Exception) {}
}
}

四、架构流程图

┌──────────────────────────────────────┐
│ InputManagerService (system_server) │
│ /dev/input/eventX │
└──────────────────┬───────────────────┘
│ socket pair

┌──────────────────────────────────────┐
│ ViewRootImpl │
│ WindowInputEventReceiver │
└──────────────────┬───────────────────┘


┌──────────────────────────────────────┐
│ mWindow.Callback │
│ .dispatchTouchEvent(event) │ ◄── 【拦截点】
│ │
│ ┌───────────────────────────┐ │
│ │ TrackingWindowCallback │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ ACTION_DOWN: │ │ │
│ │ │ 记录坐标、时间 │ │ │
│ │ │ 预查找目标 View │ │ │
│ │ └─────────────────────┘ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ ACTION_MOVE: │ │ │
│ │ │ 计算位移距离 │ │ │
│ │ │ > touchSlop → SCROLL │ │ │
│ │ └─────────────────────┘ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ ACTION_UP: │ │ │
│ │ │ 判定单击/长按/滚 │ │ │
│ │ │ 采集目标 View 信息 │ │ │
│ │ │ 异步埋点上报 │ │ │
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
│ │
│ return origin.dispatchTouchEvent() │
└──────────────────┬───────────────────┘


┌──────────────────────────────────────┐
│ Window.Callback 原始实现 │
│ (Activity / Dialog) │
│ → superDispatchTouchEvent() │
│ → ViewGroup.dispatchTouchEvent() │
│ → 子 View 分派 │
└──────────────────────────────────────┘

五、与 OnClickListener 代理方案对比

维度 OnClickListener 代理 Window.Callback 代理
拦截层级 View 层(onClick 回调) Window 层(dispatchTouchEvent)
覆盖度 仅覆盖设置了 OnClickListener 的 View 覆盖所有触摸事件(包括无 Listener 的 View)
实现方式 反射替换 mOnClickListener 委托替换 Window.Callback(公开 API)
Hidden API 风险 有(反射私有字段) 无(公开 API)
性能开销 每次点击 +1 次方法调用 每次触摸事件都须处理(包括滑动、长按)
手势区分 不需要(已有 onClick 回调) 需要自行实现滑动/点击/长按判断
坐标信息 有(可从 MotionEvent 获取精确坐标)
多窗口支持 需单独处理每种窗口 Window 级别,天然支持所有 Window
扩展性 仅点击 可扩展手势、长按、滑动轨迹等
Menu 点击 不支持 不支持(MenuItem 不经过 dispatchTouchEvent)
WebView 不支持 不支持(WebView 消费事件后不向上传递)

六、高级场景

6.1 RecyclerView 中的精准点击判断

/**
* RecyclerView 场景的点击判定优化
* 解决快速滑动时 ACTION_UP 被误判为点击的问题
*/
class RecyclerViewClickOptimizer {

// RecyclerView 滑动状态
private var scrollState = RecyclerView.SCROLL_STATE_IDLE
private var flingVelocity = 0f

fun attachToRecyclerView(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
scrollState = newState
}
})

recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_UP) {
// 如果 RecyclerView 正在 fling,ACTION_UP 实际上是停止 fling,不是点击
// 通过 scrollState 排除
}
return false
}
})
}

fun isValidClick(): Boolean {
return scrollState == RecyclerView.SCROLL_STATE_IDLE
}
}

6.2 与 GestureDetector 共存

/**
* 当业务代码使用了 GestureDetector 时,
* Window.Callback 方案的 gesture 判断可能与 GestureDetector 冲突
*
* 解决:在 dispatchTouchEvent 中将事件也传递给 GestureDetector 判断
*/
class GestureDetectorAwareCallback(
private val origin: Window.Callback,
private val gestureDetector: GestureDetector
) : Window.Callback by origin {

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

// 先让 GestureDetector 识别手势
var isGestureHandled = false
try {
gestureDetector.onTouchEvent(event).also { isGestureHandled = it }
} catch (_: Exception) {}

// 如果 GestureDetector 已经识别为点击,不再重复埋点
// ... 后续处理

return origin.dispatchTouchEvent(event)
}
}

6.3 性能优化:坐标查找的可选策略

/**
* 目标 View 查找策略选择器
*
* 三种策略:
* 1. 全量遍历:每次都深度遍历 DecorView,最准确但最慢
* 2. 缓存策略:缓存最近的查找结果,相同坐标复用(适用于不变布局)
* 3. 懒加载:只在 ACTION_UP 时查找,DOWN/MOVE 时不查找
*/
object ViewFindStrategy {

// 缓存模式
private val cache = LruCache<String, View>(maxSize = 50)

fun findView(decorView: ViewGroup, rawX: Int, rawY: Int, strategy: Strategy): View? {
return when (strategy) {
Strategy.FULL_TRAVERSAL -> findDeepestView(decorView, rawX, rawY)
Strategy.CACHED -> {
val key = "${rawX / 10}_${rawY / 10}"
cache.get(key) ?: findDeepestView(decorView, rawX, rawY)?.also {
cache.put(key, it)
}
}
Strategy.LAZY -> null // 仅在需要时才调用
}
}

enum class Strategy { FULL_TRAVERSAL, CACHED, LAZY }
}

七、集成主流分析 SDK

7.1 神策 Sensors Analytics

神策 SDK 内部也使用了 Window.Callback 代理方案来实现全埋点。其源码中 ViewCrawler 类通过替换 Window.Callback 实现自动点击追踪。

// 神策 SDK 内部的 Window.Callback 代理(简化示意)
// 实际实现位于 com.sensorsdata.analytics.android.sdk.util.WindowHelper
class SAWindowCallback(
private val origin: Window.Callback
) : Window.Callback by origin {

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

// 神策内部的全埋点逻辑
if (event.action == MotionEvent.ACTION_UP) {
SensorsDataPrivate.trackViewOnClick(event)
}

return origin.dispatchTouchEvent(event)
}
}

7.2 GrowingIO

GrowingIO 采用透明层方案(Overlay)而非 Window.Callback。两者的对比在「透明层方案」文章中详述。

八、性能指标与影响分析

8.1 坐标遍历的性能开销

对于一个包含 200 个 View 的页面,在 ACTION_DOWN 时进行一次坐标遍历:

  • 每个 View 需要执行:getLocationOnScreen()(JNI 调用)+ 边界计算
  • 总耗时:约 0.5~2ms(取决于 View 层级深度)
  • 如果每次触摸都在 ACTION_DOWN 中执行:滑动场景下每秒执行约 60 次(60fps),即每帧额外 2ms —— 可能导致丢帧

优化方案:只在 ACTION_UP 时执行全量查找,ACTION_DOWN 时只记录坐标。

8.2 内存占用

  • TrackingWindowCallback 实例:~500 bytes
  • 每个 Activity 一个实例:内存占用可忽略

九、ProGuard/R8 规则

# 保持 Window.Callback 代理类
-keep class com.example.tracking.callback.TrackingWindowCallback { *; }
-keep class com.example.tracking.callback.WindowCallbackClickCollector { *; }
-keep class com.example.tracking.callback.WindowCallbackClickHandler { *; }

# 保持注入器
-keep class com.example.tracking.callback.WindowCallbackInjector { *; }
-keep class com.example.tracking.callback.DialogCallbackInjector { *; }

# 防止 R8 移除 ActivityLifecycleCallbacks
-keep class * extends android.app.Application$ActivityLifecycleCallbacks { *; }

# Window.Callback 接口保持(防止 R8 内联)
-keep interface android.view.Window$Callback { *; }

面试常考问题

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

Kotlin 的 by 委托让代理类无需手动实现 Callback 的所有方法(20+ 个),未覆盖的方法自动转发给原始 Callback,代码量极简,只重写需要拦截的 dispatchTouchEvent。这种模式在 Java 中需要用 IDE 生成所有方法的转发代码,维护成本高且容易遗漏新版本新增的方法(如 Android 11 新增的 dispatchKeyShortcutEvent)。此外,by 委托在字节码层面生成的是静态转发调用,不存在反射或动态代理的性能开销。

Q2:如何区分 click 和 scroll?

通过 event.action 判断:记录 ACTION_DOWN 位置,ACTION_MOVE 时计算位移距离。若 sqrt(dx^2 + dy^2) < touchSlop(系统默认 8dp),继续判定为可能的点击;若位移超过阈值则切换为 SCROLL 状态。关键细节:(1)ACTION_MOVE 可能被系统批量发送(getHistoricalX/Y),需要处理历史事件;(2)ViewConfiguration.getScaledTouchSlop() 获取系统阈值;(3)多指触摸时(ACTION_POINTER_DOWN)应立即取消单击判定。此外,通过 MotionEvent.getEventTime() - getDownTime() 获取按压时长,可区分单击(< 500ms)和长按。

Q3:Dialog/PopupWindow 如何覆盖?

Dialog 有自己的 Window,需要单独 Hook。可通过 Dialog.setOnShowListener() 或在 Dialog.show() 时替换其 Window.Callback。PopupWindow 使用 WindowManager.addView() 添加窗口,其内部 contentView 的事件分发不经过 Window.Callback。对于 PopupWindow,推荐在其 contentView 上设置 OnTouchListener 或使用透明层方案覆盖。此外,AlertDialog / BottomSheetDialog 等系统组件需要特别注意:它们可能在 show() 之后才创建 DecorView。

Q4:如果同时使用了 AccessibilityDelegate 代理和 OnClickListener 代理,如何避免点击事件的重复上报?

Window.Callback 代理方案在触摸事件的最上游(Window 级别)进行拦截,而 OnClickListener 代理在 View 级别的 onClick 回调拦截。三者同时使用时,一次点击会产生三次上报。解决策略:(1)使用全局 EventBus 进行事件合并——所有方案上报到同一个 EventBus,由 EventBus 基于 timestamp + view_hash 在 200ms 窗口内去重;(2)优先级策略——Window.Callback 方案识别的点击覆盖了 OnClickListener 代理无法覆盖的「无 Listener」场景,可以设定 OnClickListener 代理采集的信息更精确(View 属性更全),Window.Callback 代理作为兜底;(3)在 Window.Callback 的 handleTouchEventBeforeDispatch 中通过反射检查目标 View 是否有 mOnClickListener,若有则跳过,将上报机会留给 OnClickListener 代理。

Q5:如何在自定义 ROM(MIUI / EMUI / ColorOS)上保证兼容性?

国产 ROM 厂商经常修改 Window 和 Activity 的实现,可能影响 Window.Callback 的替换。具体风险包括:(1)部分 ROM 在 Activity.attach() 中不是立即 setCallback(this),而是延迟设置,导致我们在 onCreate 中替换的 Callback 被 ROM 后续覆盖;(2)MIUI 的「传送门」功能会在 DecorView 上添加额外的透明层,可能拦截触摸事件;(3)部分游戏/视频 ROM 对触摸做了特殊优化,减少了 ACTION_MOVE 的频率。解决方案是:在 onActivityResumed 中再次检查并替换 Callback(防御性注入),同时对多种 ROM 做灰度测试。AOSP 源码路径:Dialog.javamWindow 属性和 Window.Callback 接口定义于 frameworks/base/core/java/android/view/Window.java

打赏
  • 微信
  • 支付宝

评论