AccessibilityDelegate 是 Android 无障碍服务的一部分,每个 View 都可以设置一个 AccessibilityDelegate 来拦截无障碍事件(包括点击)。当 TalkBack 等无障碍服务激活时,用户的触摸交互会通过 sendAccessibilityEvent 回调。利用这一机制可以实现”零代码侵入”的全埋点。
一、AOSP 源码视角:AccessibilityDelegate 的事件分发机制 1.1 AccessibilityDelegate 的定义与注册 在 frameworks/base/core/java/android/view/View.java 中,AccessibilityDelegate 通过 setAccessibilityDelegate 方法注册:
public void setAccessibilityDelegate (@Nullable AccessibilityDelegate delegate) { getListenerInfo().mAccessibilityDelegate = delegate; }
它同样存储在 ListenerInfo 内部类中,与 mOnClickListener 共享同一个容器对象。
1.2 sendAccessibilityEvent 的完整调用链 当一个无障碍事件被触发时(如 TalkBack 用户双击),调用链如下:
AccessibilityManager.sendAccessibilityEvent() → AccessibilityInteractionClient (IPC) → AccessibilityManagerService (system_server) → 遍历已注册的 AccessibilityService → AccessibilityService.onAccessibilityEvent()
但 View 层面的回调路径略有不同:
View.sendAccessibilityEvent(eventType) → View.sendAccessibilityEventUnchecked(event) → mAccessibilityDelegate.sendAccessibilityEvent(host, eventType) → mAccessibilityDelegate.sendAccessibilityEventUnchecked(host, event)
核心源码(View.java):
public void sendAccessibilityEvent (int eventType) { if (mAccessibilityDelegate != null ) { mAccessibilityDelegate.sendAccessibilityEvent(this , eventType); } else { sendAccessibilityEventInternal(eventType); } } public void sendAccessibilityEventUnchecked (AccessibilityEvent event) { if (mAccessibilityDelegate != null ) { mAccessibilityDelegate.sendAccessibilityEventUnchecked(this , event); } else { sendAccessibilityEventUncheckedInternal(event); } }
关键发现 :sendAccessibilityEvent 是一个双用途 方法。它不仅是无障碍服务的通知通道,也在普通触摸点击流程中被调用。在 View.performClick() 的最后一行:
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
这意味着:无论无障碍服务是否开启,只要 performClick() 被执行,sendAccessibilityEvent(TYPE_VIEW_CLICKED) 都会被调用 。
1.3 辟谣:AccessibilityDelegate 不需要无障碍服务开启 这是业界对该方案最大的误解。实际上,只要 View 的 performClick() 被调用(即用户点击触发了 mOnClickListener.onClick()),sendAccessibilityEvent(TYPE_VIEW_CLICKED) 就会无条件触发。因此,AccessibilityDelegate 代理方案并不依赖用户开启 TalkBack 等无障碍服务。
但要注意:如果 View 没有设置 OnClickListener,performClick() 中的 mOnClickListener != null 判断会返回 false,导致 sendAccessibilityEvent(TYPE_VIEW_CLICKED) 不会被调用。 因此该方案的覆盖范围 = 「设置了 OnClickListener 的 View」,在这一点上与 OnClickListener 代理方案相同。
二、全局代理实现 2.1 核心代理类 class TrackingAccessibilityDelegate ( private val origin: View.AccessibilityDelegate?, private val clickDelegate: ClickTrackingDelegate = DefaultClickTrackingDelegate() ) : View.AccessibilityDelegate() { override fun sendAccessibilityEvent (host: View ?, eventType: Int ) { origin?.sendAccessibilityEvent(host, eventType) if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED && host != null ) { handleClickEvent(host, AccessibilityEventType.SEND_EVENT) } super .sendAccessibilityEvent(host, eventType) } override fun sendAccessibilityEventUnchecked (host: View ?, event: AccessibilityEvent ?) { origin?.sendAccessibilityEventUnchecked(host, event) if (event?.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED && host != null ) { val extraInfo = extractAccessibilityEventInfo(event) handleClickEvent(host, AccessibilityEventType.SEND_EVENT_UNCHECKED, extraInfo) } super .sendAccessibilityEventUnchecked(host, event) } override fun onInitializeAccessibilityNodeInfo (host: View ?, info: AccessibilityNodeInfo ?) { origin?.onInitializeAccessibilityNodeInfo(host, info) super .onInitializeAccessibilityNodeInfo(host, info) } override fun onInitializeAccessibilityEvent (host: View ?, event: AccessibilityEvent ?) { origin?.onInitializeAccessibilityEvent(host, event) super .onInitializeAccessibilityEvent(host, event) } override fun onPopulateAccessibilityEvent (host: View ?, event: AccessibilityEvent ?) { origin?.onPopulateAccessibilityEvent(host, event) super .onPopulateAccessibilityEvent(host, event) } override fun onRequestSendAccessibilityEvent ( host: ViewGroup ?, child: View ?, event: AccessibilityEvent ? ) : Boolean { val originResult = origin?.onRequestSendAccessibilityEvent(host, child, event) val superResult = super .onRequestSendAccessibilityEvent(host, child, event) return originResult ?: superResult } override fun performAccessibilityAction (host: View ?, action: Int , args: Bundle ?) : Boolean { if (action == AccessibilityNodeInfo.ACTION_CLICK && host != null ) { handleClickEvent(host, AccessibilityEventType.ACCESSIBILITY_ACTION) } val originResult = origin?.performAccessibilityAction(host, action, args) val superResult = super .performAccessibilityAction(host, action, args) return originResult ?: superResult } private fun handleClickEvent ( view: View , source: AccessibilityEventType , extraInfo: Map <String , Any?> = emptyMap() ) { if (ClickDeduplicator.shouldSkip(view, source)) return clickDelegate.onViewClicked(view, source, extraInfo) } private fun extractAccessibilityEventInfo (event: AccessibilityEvent ) : Map<String, Any?> { return mapOf( "event_time" to event.eventTime, "package_name" to event.packageName?.toString(), "class_name" to event.className?.toString(), "content_description" to event.contentDescription?.toString(), "item_count" to event.itemCount, "from_index" to event.fromIndex, "added_count" to event.addedCount, "removed_count" to event.removedCount ) } } enum class AccessibilityEventType { SEND_EVENT, SEND_EVENT_UNCHECKED, ACCESSIBILITY_ACTION }
2.2 点击处理策略接口 interface ClickTrackingDelegate { fun onViewClicked (view: View , source: AccessibilityEventType , extraInfo: Map <String , Any?>) } class DefaultClickTrackingDelegate : ClickTrackingDelegate { override fun onViewClicked (view: View , source: AccessibilityEventType , extraInfo: Map <String , Any?>) { val info = buildViewClickInfo(view, source, extraInfo) when (source) { AccessibilityEventType.ACCESSIBILITY_ACTION -> { AnalyticsSDK.track("app_click" , info + mapOf( "click_source" to "accessibility_action" , "is_accessibility_user" to true )) } AccessibilityEventType.SEND_EVENT, AccessibilityEventType.SEND_EVENT_UNCHECKED -> { AnalyticsSDK.track("app_click" , info + mapOf( "click_source" to "touch" , "is_accessibility_user" to false )) } } } private fun buildViewClickInfo ( view: View , source: AccessibilityEventType , extraInfo: Map <String , Any?> ) : Map<String, Any?> { return mapOf( "view_class" to view.javaClass.name, "view_id" to view.id, "view_id_name" to resolveResourceName(view), "content_description" to (view.contentDescription?.toString() ?: "" ), "important_for_accessibility" to view.importantForAccessibility, "accessibility_class_name" to (view.accessibilityClassName?.toString() ?: "" ), "is_focused" to view.isFocused, "is_accessibility_focused" to view.isAccessibilityFocused, "click_source" to source.name, "text" to getSanitizedText(view), "page_name" to getCurrentPageName(view.context), "timestamp" to System.currentTimeMillis() ) + extraInfo } 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 getSanitizedText (view: View ) : String { return ((view as ? TextView)?.text?.toString() ?: "" ).take(100 ) } private fun getCurrentPageName (context: Context ) : String { val activity = context as ? Activity return activity?.javaClass?.simpleName ?: context.javaClass.simpleName } }
2.3 去重机制 object ClickDeduplicator { private const val DEDUP_WINDOW_MS = 200L private val lastTracked = LruCache<String, Long >(maxSize = 200 ) fun shouldSkip (view: View , source: AccessibilityEventType ) : Boolean { val key = "${view.hashCode()} _${source.name} " val now = SystemClock.elapsedRealtime() val lastTime = lastTracked.get (key) if (lastTime != null && (now - lastTime) < DEDUP_WINDOW_MS) { return true } if (source == AccessibilityEventType.SEND_EVENT_UNCHECKED) { val sendEventKey = "${view.hashCode()} _${AccessibilityEventType.SEND_EVENT.name} " val sendEventTime = lastTracked.get (sendEventKey) if (sendEventTime != null && (now - sendEventTime) < DEDUP_WINDOW_MS) { return true } } lastTracked.put(key, now) return false } }
三、全局注入方案 3.1 递归设置代理 object AccessibilityDelegateInjector { private val injectedViews = WeakHashMap<View, Boolean >() fun inject (activity: Activity ) { activity.window.decorView.post { injectRecursively(activity.window.decorView as ViewGroup) } } fun injectForView (view: View ) { if (injectedViews.containsKey(view)) return injectedViews[view] = true try { val delegateField = View::class .java.getDeclaredField("mAccessibilityDelegate" ) delegateField.isAccessible = true val currentDelegate = delegateField.get (view) as ? View.AccessibilityDelegate if (currentDelegate is TrackingAccessibilityDelegate) return view.accessibilityDelegate = TrackingAccessibilityDelegate( origin = currentDelegate, clickDelegate = DefaultClickTrackingDelegate() ) } catch (e: NoSuchFieldException) { Log.w("Tracking" , "Failed to get mAccessibilityDelegate field" , e) } catch (e: Exception) { Log.e("Tracking" , "Failed to inject AccessibilityDelegate for ${view.javaClass.name} " , e) } } private fun injectRecursively (parent: ViewGroup ) { injectForView(parent) for (i in 0 until parent.childCount) { val child = parent.getChildAt(i) injectForView(child) if (child is ViewGroup) { injectRecursively(child) } } } }
3.2 Application 级别的生命周期注册 class TrackingApplication : Application () { override fun onCreate () { super .onCreate() registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityCreated (activity: Activity , savedInstanceState: Bundle ?) { AccessibilityDelegateInjector.inject(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 ) {} }) registerFragmentCallback() } private fun registerFragmentCallback () { registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityCreated (activity: Activity , savedInstanceState: Bundle ?) { if (activity is FragmentActivity) { activity.supportFragmentManager .registerFragmentLifecycleCallbacks( object : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentViewCreated ( fm: FragmentManager , f: Fragment , v: View , savedInstanceState: Bundle ? ) { if (v is ViewGroup) { AccessibilityDelegateInjector.injectRecursively(v) } else { AccessibilityDelegateInjector.injectForView(v) } } }, true ) } } }) } }
四、架构流程图 ┌────────────────────────────┐ │ 用户在屏幕上触摸并抬起 │ └────────────┬───────────────┘ │ ▼ ┌────────────────────────────┐ │ View.dispatchTouchEvent() │ │ → onTouchEvent(ACTION_UP) │ │ → performClick() │ └────────────┬───────────────┘ │ ┌───────────┼───────────┐ │ │ ▼ ▼ ┌────────────────────┐ ┌────────────────────────┐ │ mOnClickListener │ │ sendAccessibilityEvent │ │ .onClick(this) │ │ (TYPE_VIEW_CLICKED) │ └────────────────────┘ └────────────┬───────────┘ │ ┌─────────────────────────┼─────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │ 原始 │ │ 检查 mAccessibility- │ │ 系统无障碍服务 │ │ OnClickListener │ │ Delegate 是否为 null │ │ (TalkBack等) │ └──────────────────┘ └──────────┬───────────┘ └──────────────────┘ │ ┌──────────┴──────────┐ │ != null │ == null ▼ ▼ ┌──────────────────────┐ ┌──────────────────┐ │ TrackingAccessibilit│ │ 走系统默认流程 │ │ Delegate │ │ (无埋点) │ │ .sendAccessibility- │ └──────────────────┘ │ Event(host, │ │ TYPE_VIEW_CLICKED) │ └──────────┬───────────┘ │ ┌──────────────┼──────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 去重检查 │ │ 信息采集 │ │ 埋点上报 │ │ClickDeduplic-│ │ClickInfo- │ │AnalyticsSDK. │ │ator.shouldSkip│ │Builder.build │ │track() │ └──────────────┘ └──────────────┘ └──────────────┘
五、AccessibilityDelegate vs OnClickListener 代理方案对比 5.1 详细对比矩阵
比较维度
AccessibilityDelegate
OnClickListener 代理
API 类型
公开 API,view.accessibilityDelegate = ...
私有字段 mOnClickListener,需反射
Hidden API 限制
不受影响(公开 API)
Android 11+ 受限
覆盖范围
调用 performClick() 的 View
设置 mOnClickListener 的 View
事件来源区分
可区分普通触摸 vs TalkBack 手势
无法区分
信息采集能力
AccessibilityEvent 提供丰富上下文
仅能获取 View 属性
实现复杂度
中等(递归设置 + 去重)
中等(反射 + LayoutInflater Hook)
额外副作用
可能影响无障碍功能
无副作用
多进程支持
每进程独立注入
每进程独立注入
5.2 AccessibilityDelegate 的独特优势
公开 API :setAccessibilityDelegate() 是 Android SDK 的公开方法,不受 Hidden API 限制。在 Android 11+ 设备上,这是相比反射方案的重要优势。
可区分事件源 :通过重写 performAccessibilityAction(host, ACTION_CLICK, ...) 可以识别 TalkBack 用户的手势点击,为无障碍场景的数据分析提供基础数据。
AccessibilityNodeInfo 扩展 :可以在 onInitializeAccessibilityNodeInfo 中向系统的无障碍节点树注入自定义属性,这些属性可以被其他无障碍服务消费(如测试自动化框架)。
无需操作 Listener 链 :不需要反射替换 mOnClickListener,不干扰现有的点击监听器链,降低了因反射导致 IllegalAccessException 或 NoSuchFieldException 的风险。
5.3 AccessibilityDelegate 的已知局限
**依赖 performClick()**:如果 View 的点击事件没有经过 performClick()(例如自定义 View 重写了 onTouchEvent 但没调用 super),AccessibilityDelegate 不会收到 TYPE_VIEW_CLICKED。这类场景需要使用 Window.Callback 方案覆盖。
RecyclerView 快速滚动 :在 RecyclerView 快速滚动时,View 被频繁创建和回收,递归设置 AccessibilityDelegate 可能产生性能抖动。
Fragment 生命周期 :Fragment 的 View 创建时机晚于 Activity 的 onActivityCreated,必须单独通过 FragmentLifecycleCallbacks 处理。
六、集成主流分析 SDK 6.1 神策 Sensors Analytics class SensorsAnalyticsAccessibilityDelegate ( private val origin: View.AccessibilityDelegate? ) : View.AccessibilityDelegate() { override fun sendAccessibilityEvent (host: View ?, eventType: Int ) { origin?.sendAccessibilityEvent(host, eventType) if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED && host != null ) { val properties = JSONObject().apply { put("\$element_type" , host.javaClass.simpleName) put("\$element_content" , host.contentDescription?.toString() ?: "" ) put("\$element_path" , buildViewPath(host)) put("click_source" , "accessibility_delegate" ) } SensorsDataAPI.sharedInstance().track("\$AppClick" , properties) } super .sendAccessibilityEvent(host, eventType) } }
6.2 GrowingIO class GrowingIOAccessibilityDelegate ( private val origin: View.AccessibilityDelegate? ) : View.AccessibilityDelegate() { override fun sendAccessibilityEventUnchecked (host: View ?, event: AccessibilityEvent ?) { origin?.sendAccessibilityEventUnchecked(host, event) if (event?.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED && host != null ) { val eventData = GrowingIOEventData().apply { setEventType("VIEW_CLICK" ) setPageName(getCurrentPage(host.context)) setEventVariable("view_class" , host.javaClass.name) setEventVariable("view_text" , ((host as ? TextView)?.text?.toString() ?: "" )) setEventVariable("click_source" , "accessibility_delegate" ) } GrowingIO.getInstance().track(eventData) } super .sendAccessibilityEventUnchecked(host, event) } }
七、高级场景:与自动化测试框架的结合 AccessibilityDelegate 不仅能用于埋点,还能为自动化测试提供强大的 View 查找能力。
class TestAccessibilityDelegate ( private val origin: View.AccessibilityDelegate? ) : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo (host: View ?, info: AccessibilityNodeInfo ?) { origin?.onInitializeAccessibilityNodeInfo(host, info) if (host != null && info != null ) { val testTag = host.getTag(R.id.test_tag) if (testTag != null ) { info.viewIdResourceName = "test:${testTag} " } info.isVisibleToUser = host.isShown info.isEnabled = host.isEnabled info.extras.putString("tracking_enabled" , "true" ) } super .onInitializeAccessibilityNodeInfo(host, info) } }
八、性能考量与优化 8.1 递归注入的性能分析 在复杂页面中,DecorView 可能包含数百个 View。对每个 View 进行反射获取 mAccessibilityDelegate 会产生开销。
object OptimizedInjector { private val delegateField: Field by lazy { View::class .java.getDeclaredField("mAccessibilityDelegate" ).apply { isAccessible = true } } fun injectAsync (rootView: ViewGroup , scope: CoroutineScope ) { scope.launch(Dispatchers.Main) { injectRecursivelyWithYield(rootView) } } private suspend fun injectRecursivelyWithYield (parent: ViewGroup ) { for (i in 0 until parent.childCount) { val child = parent.getChildAt(i) if (i % 10 == 0 ) { yield() } injectForViewFast(child) if (child is ViewGroup) { injectRecursivelyWithYield(child) } } } private fun injectForViewFast (view: View ) { try { val currentDelegate = delegateField.get (view) as ? View.AccessibilityDelegate if (currentDelegate !is TrackingAccessibilityDelegate) { view.accessibilityDelegate = TrackingAccessibilityDelegate(currentDelegate) } } catch (_: Exception) {} } }
8.2 内存占用分析 每个 TrackingAccessibilityDelegate 实例占用约 200 bytes(包含 origin delegate 引用、clickDelegate 引用、去重缓存条目等)。对于 500 个 View 的页面,额外内存约 100KB,属于可接受范围。
九、数据隐私与合规 9.1 ContentDescription 的敏感信息风险 许多开发者会使用 contentDescription 存储业务相关的描述文本,如 "订单号:20210601001" 或 "用户:张三"。AccessibilityDelegate 方案会采集此字段,需要脱敏处理。
object ContentDescriptionSanitizer { private val patterns = listOf( Regex("\\d{15,19}" ) to "ID_NUMBER" , Regex("1[3-9]\\d{9}" ) to "PHONE_NUMBER" , Regex("\\d{16,19}" ) to "BANK_CARD" , Regex("[\\w.-]+@[\\w.-]+" ) to "EMAIL" , ) fun sanitize (raw: String ) : String { var result = raw for ((pattern, label) in patterns) { result = pattern.replace(result, "[$label ]" ) } return result.take(200 ) } }
十、ProGuard/R8 规则 # 保持 AccessibilityDelegate 代理类 -keep class com.example.tracking.accessibility.TrackingAccessibilityDelegate { *; } -keep class com.example.tracking.accessibility.ClickTrackingDelegate { *; } -keep class com.example.tracking.accessibility.DefaultClickTrackingDelegate { *; } -keep class com.example.tracking.accessibility.ClickDeduplicator { *; } # 保持 AccessibilityDelegate 相关方法 -keepclassmembers class android.view.View { public void setAccessibilityDelegate(android.view.View$AccessibilityDelegate); android.view.View$AccessibilityDelegate mAccessibilityDelegate; } # 保持 AccessibilityEvent 类型常量 -keep class android.view.accessibility.AccessibilityEvent { public static final int TYPE_VIEW_CLICKED; } # 保持 AccessibilityNodeInfo -keep class android.view.accessibility.AccessibilityNodeInfo { public static final int ACTION_CLICK; }
面试常考问题 Q1:AccessibilityDelegate 全埋点为什么不能作为主方案?
尽管 AccessibilityDelegate 使用公开 API,不存在 Hidden API 限制,但它有三个致命缺陷作为主方案:(1)覆盖范围有限 ——它只在 performClick() 被调用时才收到 TYPE_VIEW_CLICKED,这意味着没有设置 OnClickListener 的 View(如通过 onTouchEvent 处理点击的自定义 View),以及 WebView 内的点击,都不会触发;(2)去重复杂度高 ——performClick() 会同时触发 sendAccessibilityEvent() 和 sendAccessibilityEventUnchecked(),还会触发 onPopulateAccessibilityEvent(),需要精心设计去重逻辑;(3)无法获取目标 View ——当事件来自 AccessibilityManager 而非 performClick() 时,host 参数可能不是最初被点击的 View,而是事件冒泡路径上的父 View。因此在生产环境中,AccessibilityDelegate 通常作为 OnClickListener 代理或 Window.Callback 方案的补充,而非独立的主方案。
Q2:能否通过代码强制开启无障碍服务来弥补?
不能。无障碍服务需要在系统设置中由用户手动开启,Android 出于安全和隐私考虑不允许应用通过代码自启动无障碍服务。不过有一个重要的澄清:如本文 1.3 节所述,AccessibilityDelegate 代理方案实际上并不依赖系统级无障碍服务。sendAccessibilityEvent(TYPE_VIEW_CLICKED) 在 performClick() 中被无条件调用,与系统无障碍服务的开关状态无关。真正可能影响该方案的是:部分 ROM 厂商可能在 View 层面禁用了 sendAccessibilityEvent 的调用(通过修改 AOSP 源码),但这类情况极为罕见。
Q3:如何区分真实点击和 TalkBack 模拟点击?
在 TrackingAccessibilityDelegate 的实现中,我们同时重写了 sendAccessibilityEvent 和 performAccessibilityAction。两者的调用时机不同:(1)sendAccessibilityEvent(TYPE_VIEW_CLICKED) 来自 performClick(),这意味着是真实的触摸点击(无论是否开启了 TalkBack);(2)performAccessibilityAction(ACTION_CLICK, ...) 来自 TalkBack 等无障碍服务对用户的「双击」手势的转换。通过在不同回调中设置不同的 click_source 标签,可以准确区分。此外,可以通过 AccessibilityManager.isEnabled() 判断当前是否有无障碍服务在运行,作为辅助判断条件。AOSP 源码路径:View.java 中的 performAccessibilityAction() 方法。
Q4:AccessibilityDelegate 代理方案与 Google 的 Accessibility Scanner 等无障碍测试工具有冲突吗?
合理实现的情况下不会冲突。TrackingAccessibilityDelegate 在重写每个无障碍回调时都调用了 origin?. 转发,确保原始 Delegate 的功能不受影响。但需要注意两个细节:(1)如果同时使用了多个需要设置 AccessibilityDelegate 的库(如埋点 SDK + 自动化测试 SDK),需要实现「代理链」模式,每个代理持有前一个代理的引用并转发;(2)Accessibility Scanner 等工具会检查 contentDescription 和 importantForAccessibility 属性,埋点代理不应修改这些属性的值,只应读取。
Q5:在 Fragment 中使用时,onFragmentViewCreated 和 onActivityCreated 的时序如何保证代理正确注入?
Fragment 的 View 创建发生在 Fragment.onViewCreated(),此时 Activity 的 onActivityCreated 可能尚未执行(取决于 Fragment 的添加时机)。正确的做法是双重注入:(1)在 onActivityCreated 中对 DecorView 递归注入(覆盖 Activity 自身的布局);(2)在 onFragmentViewCreated 中对 Fragment 的根 View 递归注入(覆盖 Fragment 的布局)。由于 WeakHashMap 的去重机制,同一个 View 不会被重复注入。对于 ViewPager 中懒加载的 Fragment,还需要在 Fragment.onResume() 时检查注入状态。源码参考:FragmentManager.FragmentLifecycleCallbacks 接口定义于 androidx.fragment.app.FragmentManager.java。
AOSP 源码路径:View.java 中的 setAccessibilityDelegate()、sendAccessibilityEvent()、sendAccessibilityEventUnchecked() 方法(frameworks/base/core/java/android/view/View.java)。AccessibilityDelegate 类定义于 frameworks/base/core/java/android/view/View.java 的内部类。