【Android面试】ViewModel LiveData EventBus专题
文章目录一、ViewModel1. ViewModel 配置变更不重建的底层原理是什么ViewModelStore、ViewModelStoreOwner、ViewModelProvider 三者关系与源码流程**2. ViewModel 为何能避免内存泄漏与 ViewModel 生命周期绑定的核心机制是什么onCleared() 触发时机与源码逻辑3. 自定义 ViewModelProvider.Factory 的核心作用如何实现带参构造的 ViewModel 注入源码中如何解析 Factory4. ViewModel 共享数据Fragment 间/Activity 与 Fragment的实现原理为何能做到数据同步5. ViewModel 在配置变更旋转与正常销毁两种场景下生命周期回调的源码差异是什么ViewModelStore 何时被清空**6. ViewModel 中直接使用协程Coroutine有什么风险正确的协程作用域ViewModel.viewModelScope源码原理是什么7. 当 Activity 嵌套多个 Fragment 时不同层级的 ViewModel 作用域如何划分如何实现 Activity 级、Fragment 级共享的隔离**二、LiveData1. LiveData 生命周期感知的源码实现LifecycleBoundObserver 如何绑定生命周期、感知状态变化**2. LiveData 粘性事件产生的根本原因源码层面SingleLiveEvent / UnPeekLiveData 解决粘性的核心原理**3. postValue 与 setValue 源码差异postValue 为何可能丢失数据底层 Handler 机制如何处理**4. LiveData 数据倒灌重复回调的场景与源码原因如何从设计上避免**5. LiveData 与 Kotlin Flow 本质区别生命周期感知、粘性、背压、线程切换的差异与选型6. LiveData 的 observeForever 方法为什么会导致内存泄漏它与普通 observe 在源码实现上的核心区别7. 多个 Observer 订阅同一个 LiveData 时数据分发的先后顺序由什么决定源码中如何维护 Observer 列表8.LiveData 的 dispatchingValue 与 considerNotify 两个核心方法的作用是什么如何实现数据的延迟分发9. MediatorLiveData 的源码原理是什么如何实现多个 LiveData 源的合并与监听为何能避免重复触发**三、EventBus4题核心深度1. 定义及应用场景 **2. EventBus 事件分发完整源码流程register → post → invoke → unregister线程模式如何切换3. EventBus 内存泄漏场景与原因为何必须手动 unregister与 LiveData 生命周期感知的本质差异**4. 大型项目中 EventBus 弊端耦合、可读性、调试困难替代方案LiveData、Flow、接口回调选型依据一、ViewModel1. ViewModel 配置变更不重建的底层原理是什么ViewModelStore、ViewModelStoreOwner、ViewModelProvider 三者关系与源码流程**配置变更时Activity 会重建但系统会保留其持有的 ViewModelStore新重建的 Activity 会复用同一个 ViewModelStore从而取出旧的 ViewModel 实例实现数据不丢失。三者核心关系与源码流程如下关系ViewModelStoreOwnerActivity/Fragment 实现该接口是 ViewModelStore 的持有者ViewModelStore内部以HashMap存储ViewModel实例key 为 ViewModel 类名value 为实例ViewModelProvider负责从 ViewModelStore 中获取已存在的 ViewModel若不存在则通过 ViewModelProvider.Factory 创建实例并存入 ViewModelStore。源码流程调用 ViewModelProvider.get() 方法 → 先从 ViewModelStore 的 HashMap 中根据模型类查找实例 → 查找不到则调用 Factory.create() 方法创建 ViewModel 实例 → 将创建的实例存入 ViewModelStore → 返回实例供页面使用。2. ViewModel 为何能避免内存泄漏与 ViewModel 生命周期绑定的核心机制是什么onCleared() 触发时机与源码逻辑避免内存泄漏的核心原因ViewModel 的生命周期独立于 Activity/Fragment 的视图生命周期且 ViewModel 本身不持有 View、Activity 等具有短生命周期的引用规范使用下不会因视图销毁而导致引用无法释放从而避免内存泄漏。生命周期绑定机制ViewModel 由 ViewModelStore 管理而ViewModelStore 由 ViewModelStoreOwnerActivity/Fragment持有ViewModel 的生命周期与 ViewModelStoreOwner 的“有效生命周期”Activity 从 onCreate 到 finishFragment 从 onAttach 到 detach 且不再复用绑定而非视图的销毁重建周期。onCleared() 触发时机与源码逻辑仅当 ViewModelStoreOwner 真正销毁Activity finish、Fragment 彻底 detach 且不再使用时触发配置变更屏幕旋转不会触发。源码逻辑系统在 ViewModelStoreOwner 销毁时调用 ViewModelStore.clear() 方法该方法会遍历 HashMap 中所有 ViewModel依次调用其 onCleared() 方法随后清空 HashMap释放 ViewModel 实例。3. 自定义 ViewModelProvider.Factory 的核心作用如何实现带参构造的 ViewModel 注入源码中如何解析 Factory核心作用系统默认的 ViewModelProvider.FactoryNewInstanceFactory仅能创建无参构造的 ViewModel 实例自定义 Factory 的核心作用是创建带参构造的 ViewModel如需要注入 Repository、UseCase 等依赖的 ViewModel支撑依赖注入如 Hilt、Dagger与 ViewModel 的结合使用。带参 ViewModel 注入实现自定义 Factory 实现 ViewModelProvider.Factory 接口重写 create(Class modelClass) 方法在方法内部通过反射或手动 new 的方式传入 ViewModel 所需的参数创建实例并返回。源码解析 Factory 流程ViewModelProvider 初始化时会传入 Factory调用 get() 方法获取 ViewModel 时会调用 Factory 的 create() 方法若未传入自定义 Factory会默认使用 NewInstanceFactory通过反射调用 ViewModel 的无参构造创建实例若传入自定义 Factory则执行自定义的 create() 逻辑完成带参实例的创建。4. ViewModel 共享数据Fragment 间/Activity 与 Fragment的实现原理为何能做到数据同步实现原理核心是让多个 Fragment或 Activity 与 Fragment获取同一个 ViewModelStore 中的 ViewModel 实例。具体方式Fragment 中通过 ViewModelProvider(requireActivity())[ViewModel::class.java] 获取 ViewModel此时获取的是 Activity 作为ViewModelStoreOwner 所持有的 ViewModelStore 中的实例多个 Fragment 若都通过这种方式获取得到的是同一个 ViewModel 实例。数据同步原因多个页面共享的是同一个 ViewModel 实例ViewModel 中的数据如 LiveData、普通变量是实例级别的当其中一个页面修改 ViewModel 中的数据时其他页面持有同一个实例自然能感知到数据变化实现数据同步。5. ViewModel 在配置变更旋转与正常销毁两种场景下生命周期回调的源码差异是什么ViewModelStore 何时被清空**生命周期回调差异配置变更屏幕旋转Activity/Fragment 会销毁重建但 ViewModelStore 会被系统ActivityClientRecord保留ViewModel 不会被销毁也不会调用 onCleared() 方法新重建的页面会复用原有的 ViewModelStore 和 ViewModel 实例。正常销毁(Activity finish、Fragment 彻底 detach)ViewModelStoreOwner 会被销毁系统会调用 ViewModelStore.clear() 方法遍历调用所有 ViewModel 的 onCleared() 方法随后清空 ViewModelStore 中的所有实例ViewModel 被销毁。ViewModelStore 清空时机仅当 ViewModelStoreOwner 真正销毁Activity.isFinishing() true、Fragment 彻底 detach 且不再复用时系统才会调用 ViewModelStore.clear() 清空 Store配置变更时不会清空。6. ViewModel 中直接使用协程Coroutine有什么风险正确的协程作用域ViewModel.viewModelScope源码原理是什么直接使用协程的风险若在 ViewModel 中手动创建 CoroutineScope如 GlobalScope、自定义 Scope未绑定 ViewModel 生命周期当 ViewModel 被销毁onCleared() 触发时协程若未完成会继续执行且协程可能持有 Activity/Fragment 的引用导致内存泄漏同时无用的协程继续执行会浪费资源。viewModelScope 源码原理viewModelScope 是 ViewModel 的扩展属性内部基于 SupervisorJob() 和 Dispatchers.Main 构建 CoroutineScopeViewModel 内部会监听自身的生命周期当 onCleared() 触发时会自动调用 viewModelScope.cancel() 方法取消所有未完成的协程从而保证协程与 ViewModel 生命周期同步避免泄漏和资源浪费。7. 当 Activity 嵌套多个 Fragment 时不同层级的 ViewModel 作用域如何划分如何实现 Activity 级、Fragment 级共享的隔离**作用域划分ViewModel 的作用域由其所属的 ViewModelStoreOwner 决定核心划分3类Activity 级作用域ViewModelStoreOwner 为 Activity通过 ViewModelProvider(requireActivity()) 获取供 Activity 及所有子 Fragment 共享。Fragment 自身作用域ViewModelStoreOwner 为当前 Fragment通过 ViewModelProvider(this) 或 viewModels() 方法获取仅当前 Fragment 可使用子 Fragment 无法共享。父 Fragment 级作用域ViewModelStoreOwner 为父 Fragment通过 ViewModelProvider(parentFragment!!) 获取供父 Fragment 及其子 Fragment 共享与 Activity 级、其他 Fragment 级隔离。隔离实现不同 ViewModelStoreOwner 持有各自独立的 ViewModelStore不同 Store 中的 ViewModel 实例互不干扰通过选择不同的 ViewModelStoreOwnerActivity、当前 Fragment、父 Fragment获取 ViewModel即可实现不同层级的共享与隔离。二、LiveData1. LiveData 生命周期感知的源码实现LifecycleBoundObserver 如何绑定生命周期、感知状态变化**生命周期感知核心源码实现LiveData 的 observe() 方法接收 LifecycleOwnerActivity/Fragment和 Observer内部会将 Observer 包装成 LifecycleBoundObserver 实例LifecycleBoundObserver 同时实现了 LifecycleObserver 和 Observer 接口通过绑定 LifecycleOwner 的生命周期实现感知。LifecycleBoundObserver 绑定与感知逻辑绑定在 observe() 方法中将 LifecycleBoundObserver 注册到 LifecycleOwner 的 Lifecycle 中lifecycle.addObserver(this)。感知状态变化LifecycleBoundObserver 重写了onStateChanged()方法监听 Lifecycle 的状态变化当 Lifecycle 状态为 STARTED 或 RESUMED 时标记为活跃状态LiveData 会分发数据给该 Observer当 Lifecycle 状态为 DESTROYED 时自动调用 LiveData.removeObserver()移除当前 Observer避免内存泄漏。2. LiveData 粘性事件产生的根本原因源码层面SingleLiveEvent / UnPeekLiveData 解决粘性的核心原理**定义当一个观察者Observer订阅了一个 LiveData 实例时它会自动接收到最近一次设置的值即“粘住”了最后一次值即使这个值是在观察者订阅之前设置的粘性事件根本原因源码层面LiveData 内部维护了 mData当前数据和mVersion数据版本号两个变量当新的 Observer 注册时LiveData 会调用 considerNotify() 方法对比 Observer 的版本号与 LiveData 的 mVersion若 Observer 的版本号小于 mVersion会立刻将当前 mData 分发给该 Observer即“先发送数据后注册 Observer”Observer 仍能收到之前发送的数据形成粘性事件。解决原理SingleLiveEvent继承 MutableLiveData内部添加一个 boolean 类型的标志位如 mPending标记数据是否已被消费当 Observer 收到数据并处理后将标志位置为 false下次注册 Observer 时若标志位为 false不分发历史数据仅分发新数据实现单次消费解决粘性。UnPeekLiveData为每个 Observer 维护独立的版本号而非共用 LiveData 的 mVersion当 Observer 注册时将其版本号初始化为与 LiveData 的 mVersion 一致只有当 LiveData 的 mVersion 大于该 Observer 的版本号时才分发数据从而阻止“订阅前的旧数据”分发彻底解决粘性。3. postValue 与 setValue 源码差异postValue 为何可能丢失数据底层 Handler 机制如何处理**源码差异setValue仅能在主线程调用直接给 mData 赋值同时 incrementVersion()递增 mVersion随后调用 dispatchingValue() 方法分发数据同步执行无延迟。postValue可在子线程调用内部通过 ArchTaskExecutor.getMainThreadExecutor()底层是 Handler将赋值操作切换到主线程先将数据存入 mPendingData临时变量再发送 Handler 消息主线程收到消息后调用 setValue(mPendingData)完成数据赋值与分发异步执行。postValue 丢失数据原因若短时间内连续多次调用 postValue会多次修改 mPendingData 的值但 Handler 消息队列中仅会保留最后一次消息前序消息会被覆盖主线程执行消息时仅会处理最后一次 mPendingData 的值导致前序 postValue 的数据被丢失。底层 Handler 处理逻辑postValue 内部通过 Handler 发送一个 Runnable 任务到主线程消息队列该任务的核心逻辑是调用 setValue()将 mPendingData 的值赋给 mData 并分发由于 Handler 消息是串行执行的且多次 postValue 会覆盖 mPendingData因此仅最后一次数据会被分发。4. LiveData 数据倒灌重复回调的场景与源码原因如何从设计上避免**数据倒灌场景页面重建如屏幕旋转、Fragment 重新 attach时Observer 重新注册到 LiveData会再次收到之前已经分发过的旧数据导致回调重复执行如重复请求接口、重复更新 UI。源码原因与粘性事件根源一致LiveData 会保存当前 mData 和 mVersion页面重建后新的 Observer 注册时其版本号初始化为 0小于 LiveData 的 mVersionconsiderNotify() 方法会判断版本差触发数据分发导致旧数据重复回调。设计上避免方案事件类数据如点击事件、接口请求结果使用SingleLiveEvent、UnPeekLiveData实现数据单次消费避免重复回调。状态类数据如 UI 显示状态、列表数据区分“状态初始化”与“状态更新”在 Observer 中添加判断如判断数据是否为初始值、是否与当前 UI 状态一致避免重复处理。替代方案使用Kotlin StateFlow其为冷数据流仅在订阅后才会分发数据天然避免数据倒灌。5. LiveData 与 Kotlin Flow 本质区别生命周期感知、粘性、背压、线程切换的差异与选型本质区别LiveData 是“生命周期感知的状态持有者”核心用于持有 UI 状态被动分发数据Kotlin Flow 是“异步数据流”核心用于处理异步操作、数据流转主动发射数据支持丰富的操作符。核心差异对比生命周期感知LiveData 天然支持无需额外处理Flow 本身不支持需通过 repeatOnLifecycle() 或 lifecycleScope 绑定生命周期否则可能导致内存泄漏。粘性LiveData 天然有粘性保存最新数据新订阅者会收到旧数据Flow 是冷流无粘性订阅前的发射数据不会被接收SharedFlow 可配置粘性但默认无粘性。背压LiveData 无背压机制若数据分发速度快于 Observer 处理速度会导致数据堆积Flow 支持背压如 buffer、conflate 等操作符可灵活处理数据生产与消费的速度差。线程切换LiveData 仅能在主线程分发数据线程切换需在数据源头处理Flow 可通过 flowOn() 自由切换发射线程通过 collect() 切换收集线程操作更灵活。选型建议简单 UI 状态如开关状态、文本显示、无需复杂异步操作用 LiveData复杂异步操作如多接口联动、数据转换、需要背压、多源数据合并用 Flow/StateFlow/SharedFlow页面内状态用 StateFlow跨页面通信用 SharedFlow。6. LiveData 的 observeForever 方法为什么会导致内存泄漏它与普通 observe 在源码实现上的核心区别内存泄漏原因observeForever() 方法不接收 LifecycleOwner 参数内部会将 Observer 包装成 AlwaysActiveObserver 实例该实例不会绑定任何生命周期始终处于活跃状态当页面Activity/Fragment销毁时AlwaysActiveObserver 依然持有页面的引用且 LiveData 不会自动移除该 Observer导致页面无法被 GC 回收产生内存泄漏。与普通 observe 的源码区别普通 observe包装成 LifecycleBoundObserver绑定 LifecycleOwner监听生命周期DESTROYED 时自动移除 Observer无泄漏风险。observeForever包装成 AlwaysActiveObserver不绑定任何 Lifecycle始终处于活跃状态LiveData 不会自动移除该 Observer必须手动调用 removeObserver()否则必泄漏。7. 多个 Observer 订阅同一个 LiveData 时数据分发的先后顺序由什么决定源码中如何维护 Observer 列表分发顺序多个 Observer 订阅同一个 LiveData 时数据分发的先后顺序由“注册顺序”决定先注册的 Observer 先收到数据后注册的后收到。源码中 Observer 列表维护LiveData 内部使用 SafeIterableMap一个线程安全的有序映射表存储 Observer 及其包装类LifecycleBoundObserver/AlwaysActiveObserverSafeIterableMap 会按 Observer 的注册顺序存储遍历分发数据时会按照存储顺序依次调用每个 Observer 的 onChanged() 方法保证分发顺序与注册顺序一致同时SafeIterableMap 支持并发修改如一边分发一边移除 Observer避免并发异常。8.LiveData 的 dispatchingValue 与 considerNotify 两个核心方法的作用是什么如何实现数据的延迟分发核心方法作用dispatchingValue控制数据分发的流程防止递归重入如 Observer 中修改 LiveData 数据导致再次触发分发内部通过 mDispatchingValue 布尔变量标记是否正在分发若正在分发则将数据加入 mPendingData待当前分发完成后再处理实现数据的有序分发。considerNotify判断是否需要向 Observer 分发数据是数据分发的核心判断逻辑主要做两件事① 判断 Observer 对应的生命周期是否处于活跃状态STARTED/RESUMED② 对比 Observer 的版本号与 LiveData 的 mVersion若 Observer 版本号小于 LiveData 版本号才会调用 Observer.onChanged() 分发数据。延迟分发实现当 LiveData 调用 setValue/postValue 时若此时 Observer 处于非活跃状态如页面后台considerNotify() 会判断生命周期不活跃不立即分发数据当页面重新回到活跃状态如从后台切回前台LifecycleBoundObserver 会感知到状态变化触发 LiveData 再次调用 dispatchingValue() 和 considerNotify()此时生命周期活跃完成数据分发实现延迟分发。9. MediatorLiveData 的源码原理是什么如何实现多个 LiveData 源的合并与监听为何能避免重复触发**源码原理MediatorLiveData 继承自 MutableLiveData内部维护了一个HashMapmSources用于存储添加的 LiveData 源Source及其对应的 Observer每个添加的 LiveData 源都会被包装成 Source 实例Source 内部持有一个 Observer用于监听该 LiveData 源的数据变化当数据变化时会调用 MediatorLiveData 的 onChanged() 方法将数据转发给 MediatorLiveData 的观察者。多源合并与监听实现通过 addSource() 方法添加多个 LiveData 源addSource() 方法会创建 Source 实例将该实例的 Observer注册到对应的 LiveData 源上当任一 LiveData 源的数据发生变化时其对应的 Observer 会被触发进而调用 MediatorLiveData 的 onChanged()MediatorLiveData 再将数据分发给自己的观察者实现多源合并与监听。避免重复触发原因MediatorLiveData 会通过版本号控制数据分发每个 Source 对应的 LiveData 源都有自己的版本号MediatorLiveData 会记录每个源的最新版本当某个 LiveData 源的数据未发生变化版本号未递增时不会触发转发从而避免重复触发 MediatorLiveData 的观察者。三、EventBus4题核心深度1. 定义及应用场景 **定义EventBus 是一个基于发布-订阅模式的事件总线库主要用于在 Android应用中进行组件间通信解耦。它由 GreenRobot 开发并维护广泛用于早期 Android 架构中以简化不同组件如 Activity、Fragment、Service 等之间的通信。应用场景组件间通信全局事件广播用户登录状态变更、全局网络状态变化解耦业务逻辑2. EventBus 事件分发完整源码流程register → post → invoke → unregister线程模式如何切换完整分发流程① register注册调用 EventBus.register(subscriber) → 查找订阅者的所有订阅方法通过索引或反射→ 将订阅方法信息事件类型、线程模式、优先级等存入缓存如 subscriberMethodFinder、subscriptionsByEventType→ 建立“事件类型 → 订阅方法列表”的映射。② post发送事件调用 EventBus.post(event) → 将事件加入事件队列PendingPostQueue→ 调用 postSingleEvent() 方法根据事件类型查找对应的订阅方法列表 → 遍历订阅方法列表调用 postToSubscription() 方法准备分发事件。③ invoke调用订阅方法postToSubscription() 方法根据订阅方法的线程模式切换到对应线程 → 通过反射调用订阅者的订阅方法将事件传入完成事件分发。④ unregister解绑调用 EventBus.unregister(subscriber) → 从缓存中移除该订阅者的所有订阅方法信息 → 解除“事件类型 → 订阅方法”的映射避免内存泄漏。线程模式切换逻辑POSTING同线程分发直接在 post() 调用的线程中反射调用订阅方法无线程切换。MAIN主线程分发通过 Handler 发送消息到主线程在主线程中调用订阅方法。BACKGROUND子线程分发若当前线程不是主线程直接调用若是主线程将任务加入后台线程池在子线程中执行。ASYNC独立新线程分发无论当前线程是什么都创建一个新的独立线程调用订阅方法执行完成后销毁线程。3. EventBus 内存泄漏场景与原因为何必须手动 unregister与 LiveData 生命周期感知的本质差异**内存泄漏场景与原因最常见场景是 Activity/Fragment 注册 EventBus 后未在 onDestroy() 中调用 unregister()原因EventBus 内部会通过缓存subscriptionsByEventType强引用订阅者Activity/Fragment当 Activity/Fragment 销毁时EventBus 的强引用依然存在导致 Activity/Fragment 无法被 GC 回收产生内存泄漏。必须手动 unregister 的原因EventBus 本身没有生命周期感知能力无法判断订阅者如 Activity是否已经销毁无法自动移除对订阅者的强引用只有通过手动调用 unregister()才能从 EventBus 的缓存中移除订阅者信息释放强引用避免内存泄漏。与 LiveData 生命周期感知的本质差异LiveData 依赖 Lifecycle 组件通过 LifecycleBoundObserver 绑定 LifecycleOwner 的生命周期能自动感知页面销毁自动移除 Observer从机制上杜绝泄漏而 EventBus 无任何生命周期感知机制完全依赖开发者手动管理注册与解绑若遗漏则必然导致泄漏。4. 大型项目中 EventBus 弊端耦合、可读性、调试困难替代方案LiveData、Flow、接口回调选型依据大型项目中 EventBus 核心弊端耦合度高事件是全局的发送方与接收方无直接关联一个事件的修改可能影响多个接收方代码耦合度高维护成本高。可读性差无法快速定位一个事件的发送方和接收方代码逻辑分散后期排查问题时难以追踪事件流转路径。调试困难事件分发是异步的若出现异常难以定位异常发生在发送端还是接收端调试效率低。易遗漏解绑大型项目中页面多容易遗漏 unregister()导致内存泄漏排查难度大。替代方案选型依据接口回调适用于“一对一”的简单通信如 Activity 与 Fragment 通信、回调接口返回结果耦合度低、可读性强适合简单场景。LiveData适用于页面内Activity/Fragment的状态管理、数据同步天然生命周期安全无泄漏风险适合 UI 相关的状态通信。**Kotlin Flow**StateFlow/SharedFlow适用于复杂异步通信、多源数据合并、跨页面通信StateFlow 适合页面内状态管理SharedFlow 适合跨页面全局事件通信支持背压灵活性高是大型项目的首选。补充跨进程通信不适合用 EventBus优先使用 BroadcastReceiver、AIDL 等方案。