Window.Callback 代理方案是在更底层——Window 级别拦截所有触摸事件,从中识别点击行为并完成埋点。相比 OnClickListener 代理,它覆盖了所有 View(包括动态创建和无 Listener 的 View),且不依赖反射操作私有字段。
一、AOSP 源码视角:Window.Callback 的完整协议 1.1 Window.Callback 接口定义 在 frameworks/base/core/java/android/view/Window.java 中,Window.Callback 接口定义了以下方法:
public interface Callback { public boolean dispatchKeyEvent (KeyEvent event) ; 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) ; 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) ; public ActionMode onWindowStartingActionMode (ActionMode.Callback callback) ; public ActionMode onWindowStartingActionMode (ActionMode.Callback callback, int type) ; public boolean onSearchRequested () ; public boolean onSearchRequested (SearchEvent searchEvent) ; 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() 方法中:
final void attach (Context context, ActivityThread aThread, ...) { mWindow = new PhoneWindow (this , window, activityConfigCallback); mWindow.setCallback(this ); }
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 树的分发尚未开始。这意味着我们有两种点击识别策略:
事件层面识别 :仅根据 MotionEvent 的坐标和时序判断是否为点击(无需查找目标 View)。
目标 View 查找 :在 ACTION_UP 时通过坐标遍历 DecorView 找到目标 View,获取其属性信息。
1.4 Kotlin 委托模式(by 关键字)的优势 使用 Kotlin 的类委托(Class Delegation)可以极大简化代理类的实现:
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 核心代理类 class TrackingWindowCallback ( private val origin: Window.Callback ) : Window.Callback by origin { private var downRawX = Float .NaN private var downRawY = Float .NaN private var downTime = 0L private var downView: WeakReference<View>? = null private var touchSlop = 0 private var mayBeLongClick = false private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong() 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 { } override fun dispatchTouchEvent (event: MotionEvent ?) : Boolean { event ?: return origin.dispatchTouchEvent(null ) handleTouchEventBeforeDispatch(event) 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 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() } } } private fun handleTouchEventAfterDispatch (event: MotionEvent , handled: Boolean ) { when (event.actionMasked) { MotionEvent.ACTION_UP -> { val elapsed = SystemClock.elapsedRealtime() - downTime if (gestureType == GestureType.UNKNOWN) { 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() } } } 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? { for (i in parent.childCount - 1 downTo 0 ) { val child = parent.getChildAt(i) ?: continue 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) { 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 版本) class WindowCallbackClickCollector ( private val decorViewSupplier: () -> ViewGroup? ) { fun collect (view: View , event: MotionEvent ) : Map<String, Any?> { return buildMap { 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 ]) 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) 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)) 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 ) ?: "" } } 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("/" ) } 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) } } class ClickDeduplicator { private data class ClickKey ( val viewHash: Int , val xBucket: Int , val yBucket: Int , val timeBucket: Int ) 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()) } } class WindowCallbackInjector : Application.ActivityLifecycleCallbacks { 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 注入 object DialogCallbackInjector { private val injectedDialogs = WeakHashMap<Dialog, Boolean >() 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 ) }
object PopupWindowCallbackInjector { fun inject (popupWindow: PopupWindow ) { try { val popupField = PopupWindow::class .java.getDeclaredField("mPopup" ) popupField.isAccessible = true val popup = popupField.get (popupWindow) val contentView = popupWindow.contentView ?: return } 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 中的精准点击判断 class RecyclerViewClickOptimizer { 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) { } return false } }) } fun isValidClick () : Boolean { return scrollState == RecyclerView.SCROLL_STATE_IDLE } }
6.2 与 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 ) var isGestureHandled = false try { gestureDetector.onTouchEvent(event).also { isGestureHandled = it } } catch (_: Exception) {} return origin.dispatchTouchEvent(event) } }
6.3 性能优化:坐标查找的可选策略 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 实现自动点击追踪。
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.java 的 mWindow 属性和 Window.Callback 接口定义于 frameworks/base/core/java/android/view/Window.java。