Android面试总结之jet pack模块化组件篇
一、ViewModel 深入问题
1. ViewModel 如何实现跨 Fragment 共享数据?其作用域是基于 Activity 还是 Fragment?
问题解析:
ViewModel 的作用域由 ViewModelStoreOwner 决定。当 Activity 和其内部 Fragment 共享同一个 ViewModelStoreOwner(如 Activity 本身)时,Fragment 可通过 Activity 的 ViewModelStore 共享 ViewModel。
源码分析:
ViewModelProvider的构造函数接收ViewModelStoreOwner(如 Activity 或 Fragment),每个 Owner 有独立的ViewModelStore。- Activity 的 ViewModelStore:存储在
ComponentActivity的mViewModelStore成员变量,配置变更时通过NonConfigurationInstance保存(见Activity#onRetainNonConfigurationInstance)。 - Fragment 的 ViewModelStore:若在 Fragment 中通过
requireActivity()作为 Owner 创建 ViewModel(如new ViewModelProvider(requireActivity()).get(...)),则共享 Activity 的 ViewModelStore;若用this作为 Owner,则使用 Fragment 独立的 ViewModelStore(仅在 Fragment 销毁时清除)。
总结:
跨 Fragment 共享数据时,需让 Fragment 使用同一个 Owner(如 Activity)获取 ViewModel,此时 ViewModel 作用域为 Activity 生命周期;若用 Fragment 自身作为 Owner,则作用域为 Fragment 生命周期。
2. ViewModel 的 onCleared () 何时被调用?如何避免内存泄漏?
问题解析:
onCleared() 在 ViewModelStore 调用 clear() 时触发,需确保在此释放资源(如取消协程、解绑回调)。
源码分析:
ComponentActivity#onDestroy()中,若!isChangingConfigurations()(即 Activity 真正销毁,非配置变更),则调用getViewModelStore().clear()。ViewModelStore#clear()遍历所有 ViewModel 并调用onCleared(),同时清空 HashMap。- 若 ViewModel 持有 Activity 引用(如非 Application 上下文),需使用弱引用或
getApplication()避免泄漏。
总结:
onCleared() 在 Owner(Activity/Fragment)永久销毁时调用,需在此清理异步任务(如取消 viewModelScope 中的协程),避免持有 Activity 强引用。
3. 如何自定义 ViewModelProvider.Factory?其在依赖注入中的作用是什么?
问题解析:
自定义 Factory 可向 ViewModel 传递参数(如数据库实例、Repository),是依赖注入的关键。
源码分析:
ViewModelProvider构造函数接收Factory,默认使用NewInstanceFactory(通过无参构造创建 ViewModel)。- 自定义 Factory 需实现
create(Class<T> modelClass),例如:public class MyViewModelFactory extends ViewModelProvider.Factory {private final Context context;public MyViewModelFactory(Context context) {this.context = context;}@NonNull@Overridepublic <T extends ViewModel> T create(@NonNull Class<T> modelClass) {if (modelClass.isAssignableFrom(MyViewModel.class)) {return (T) new MyViewModel(context); // 传递参数}throw new IllegalArgumentException("Unknown ViewModel class");} } - 在 Activity 中使用:
new ViewModelProvider(this, new MyViewModelFactory(getApplication())).get(MyViewModel.class);
总结:
自定义 Factory 允许向 ViewModel 注入依赖,避免硬编码,配合 Hilt 等库可实现更复杂的依赖管理。
二、LiveData 深入问题
1. LiveData 为什么会出现 “黏性事件”?如何实现非黏性订阅?
问题解析:
黏性事件指新订阅的观察者会立即收到最新数据,即使数据未更新。这是由于 LiveData 存储了最新数据和版本号。
源码分析:
LiveData内部通过mData存储数据,mVersion记录版本号。observe()注册时,LifecycleBoundObserver的considerNotify()方法会检查观察者的mLastVersion是否小于当前mVersion,若小于则触发onChanged():if (observer.mLastVersion >= mVersion) {return; // 版本号一致,不通知 } observer.mLastVersion = mVersion; ((Observer<T>) observer.mObserver).onChanged((T) mData);- 新订阅的观察者
mLastVersion初始为-1,必然小于当前mVersion(至少为 0),导致立即触发回调。
非黏性实现:
- 使用
observeForever(Observer)配合生命周期监听,手动控制订阅与取消。 - 或封装
MediatorLiveData或Transformations.map()过滤旧数据。
总结:
黏性事件是 LiveData 设计用于保证 UI 一致性的机制,若需非黏性(如事件只触发一次),可使用 SingleLiveEvent 或第三方库(如 EventFlow)。
2. setValue () 和 postValue () 的区别是什么?如何保证线程安全?
问题解析:
二者均用于更新数据,区别在于线程调度和执行时机。
源码分析:
setValue():- 必须在主线程调用(通过
assertMainThread()检查)。 - 直接更新
mData和mVersion,调用dispatchingValue()通知观察者。
- 必须在主线程调用(通过
postValue():- 可在子线程调用,通过
ArchTaskExecutor将任务切换到主线程。 - 数据暂存到
mPendingData,通过mPostValueRunnable异步执行setValue():private final Runnable mPostValueRunnable = new Runnable() {@Overridepublic void run() {Object newValue;synchronized (mDataLock) {newValue = mPendingData;mPendingData = NOT_SET;}setValue((T) newValue);} };
- 可在子线程调用,通过
- 线程安全由
synchronized (mDataLock)保证,避免多线程同时修改mPendingData。
总结:
setValue() 用于主线程即时更新,postValue() 用于子线程异步更新,二者最终都会通过 dispatchingValue() 通知活跃观察者。
3. LiveData 如何感知 LifecycleOwner 的生命周期状态?
问题解析:
通过 LifecycleBoundObserver 监听 LifecycleOwner 的状态变化,控制观察者的活跃状态。
源码分析:
observe()方法中创建LifecycleBoundObserver,其实现LifecycleEventObserver,重写onStateChanged():class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {final LifecycleOwner mOwner;@Overridepublic void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {if (source.getLifecycle().getCurrentState() == DESTROYED) {removeObserver(mObserver); // 销毁时移除观察者return;}activeStateChanged(shouldBeActive()); // 更新活跃状态}private boolean shouldBeActive() {Lifecycle.State state = mOwner.getLifecycle().getCurrentState();return state.isAtLeast(Lifecycle.State.STARTED); // STARTED 或 RESUMED 时活跃} }- 当 LifecycleOwner 状态变为
STARTED或RESUMED时,观察者变为活跃,接收数据更新;DESTROYED时自动移除,避免内存泄漏。
总结:
通过将观察者与 LifecycleOwner 的生命周期绑定,LiveData 确保仅在组件活跃时通知数据变化,实现自动的订阅 / 取消订阅。
三、Room 深入问题
1. Room 如何实现协程支持?suspend 函数的底层原理是什么?
问题解析:
Room 通过编译时生成的代码,将协程挂起函数转换为后台线程执行的任务。
源码分析:
- 在 DAO 中定义
suspend函数时,Room 生成的实现类会使用kotlinx.coroutines.CoroutineDispatcher。 - 例如,
NewsDao中suspend fun insertNews(News)会被编译为:public final class NewsDao_Impl implements NewsDao {private final RoomDatabase __db;private final CoroutineDispatcher __dispatcher = Dispatchers.IO; // 默认使用 IO 线程@Overridepublic void insertNews(final News news) {Coroutines.async(__dispatcher, false, new Continuation<Object, Unit>() {// 执行 SQL 插入操作(在后台线程)});} } - 可通过
@Query(workerThread = true)或在数据库构建时指定Dispatcher自定义线程。
总结:
Room 利用 Kotlin 协程的 suspend 特性,默认在 Dispatchers.IO 线程执行数据库操作,避免阻塞主线程,编译时生成的代码确保线程安全。
2. Room 如何处理数据库升级?版本冲突时的最佳实践是什么?
问题解析:
通过 @Database(version = X) 指定版本,升级时需提供 Migration 对象定义升级逻辑。
源码分析:
RoomDatabaseBuilder调用build()时,会检查数据库版本:if (databaseVersion > existingVersion) {applyMigration(migrations, existingVersion, databaseVersion); // 应用 Migration }Migration实现migrate(SupportSQLiteDatabase, int oldVersion, int newVersion),需手动编写 SQL 语句修改表结构,例如:static final Migration MIGRATION_1_2 = new Migration(1, 2) {@Overridepublic void migrate(@NonNull SupportSQLiteDatabase database) {database.execSQL("ALTER TABLE News ADD COLUMN content TEXT"); // 添加字段} };- 若新旧版本之间无对应 Migration,会抛出
IncompatibleDbException。
最佳实践:
- 每次升级仅处理相邻版本(如 1→2,2→3),避免跨版本升级。
- 复杂升级可先备份数据,删除旧表并创建新表(
fallbackToDestructiveMigration)。
总结:
Room 通过 Migration 类支持数据库升级,需确保每个版本间的迁移逻辑正确,避免数据丢失。
3. Room 如何优化查询性能?是否支持索引和事务?
问题解析:
Room 支持 SQL 优化特性,如索引、事务、批量操作等。
源码分析:
- 索引:通过
@Index注解为实体类字段添加索引,生成的 SQL 会包含CREATE INDEX。 - 事务:在 DAO 中使用
@Transaction注解标记方法,Room 会生成beginTransaction()和endTransaction()包裹操作:@Dao interface NewsDao {@Transactionsuspend fun insertAndUpdate(News news1, News news2) {insertNews(news1);updateNews(news2); // 保证原子性} } - 批量操作:使用
@Insert(onConflict = REPLACE)或直接插入列表(insertAll(List<News>)),减少多次 IO 开销。
总结:
Room 直接支持 SQL 优化特性,合理使用索引和事务可显著提升性能,批量操作避免逐行插入的性能损耗。
四、Navigation 深入问题
1. NavController 如何管理返回栈?popUpTo 和 popUpToInclusive 的作用是什么?
问题解析:
返回栈由 NavController 维护,通过导航图中的 action 配置栈行为。
源码分析:
NavController内部使用BackStack类(实为ArrayDeque<BackStackEntry>)记录导航历史。popUpTo属性指定返回时需弹出的目标目的地 ID,popUpToInclusive若为true,则同时弹出目标本身:<actionandroid:id="@+id/action_A_to_B"app:destination="@id/B"app:popUpTo="@id/A"app:popUpToInclusive="false" /> <!-- 从 B 返回时弹出到 A(不包含 A) -->- 导航时,若
popUpTo存在,NavController 会先弹出栈中所有在目标 ID 之上的条目,再压入新目的地。
总结:
popUpTo 用于清理返回栈,避免冗余条目,popUpToInclusive 控制是否包含目标条目,确保导航逻辑符合预期。
2. 如何在导航中传递复杂参数?是否支持安全类型校验?
问题解析:
通过导航图的 argument 定义参数,Room 支持类型校验和自动序列化。
源码分析:
在导航图中定义参数:
<fragmentandroid:id="@+id/DetailFragment"android:name="com.example.DetailFragment"><argumentandroid:name="newsId"app:argType="integer"android:defaultValue="-1" />
</fragment>
NavController.navigate(R.id.DetailFragment, bundle)传递参数,findNavController().getCurrentBackStackEntry().getArguments()获取,编译时通过@NavigationRes校验目标 ID 合法性。- 复杂对象需实现
Parcelable或使用@TypeConverter(如 Room 实体类),导航时自动序列化 / 反序列化。
总结:
Navigation 支持基本类型和 Parcelable 对象的参数传递,编译时校验目标 ID,确保类型安全,复杂对象需实现序列化接口。
3. 深层链接(Deep Link)如何与 Navigation 集成?原理是什么?
问题解析:
深层链接通过导航图配置,将 URL 映射到应用内目的地。
源码分析:
- 在导航图中添加
deepLink标签:<fragmentandroid:id="@+id/DetailFragment"><deepLinkandroid:id="@+id/deepLink"app:uri="http://example.com/news/{newsId}" /> </fragment> NavDeepLinkBuilder解析 URL,通过NavController.navigate(uri)匹配导航图中的deepLink规则:NavDeepLinkBuilder(this).setGraph(R.navigation.navigation_graph).setUri(uri).createTask().addOnSuccessListener(navController -> navController.navigate(deepLinkMatch.getDestination().getId()));- 核心是
NavDeepLinkMatcher类,将 URL 路径与导航图中的deepLink模式匹配,提取参数。
总结
一、ViewModel 核心总结
-
跨 Fragment 数据共享
- 作用域:由
ViewModelStoreOwner决定,通过 Activity 作为 Owner 可实现跨 Fragment 共享(作用域为 Activity 生命周期),若用 Fragment 自身作为 Owner 则作用域为 Fragment 生命周期。 - 原理:Activity 的
ViewModelStore在配置变更时通过NonConfigurationInstance保存,避免 ViewModel 重建。
- 作用域:由
-
生命周期与内存泄漏
onCleared()在 Owner 永久销毁(非配置变更)时调用,需在此清理异步任务(如取消viewModelScope协程)、释放资源。- 避坑:避免在 ViewModel 中持有 Activity 强引用,改用
getApplication()获取上下文。
-
自定义 Factory 与依赖注入
- 实现
ViewModelProvider.Factory可向 ViewModel 传递参数(如 Repository、数据库实例),是手动依赖注入的基础,配合 Hilt 可实现全自动依赖管理。
- 实现
二、LiveData 核心总结
-
黏性事件与非黏性实现
- 原因:通过版本号
mVersion机制,新订阅者因初始版本号为-1,必然触发最新数据回调。 - 非黏性方案:使用
observeForever()手动管理订阅,或封装SingleLiveEvent(单次触发)、结合MediatorLiveData过滤旧数据。
- 原因:通过版本号
-
线程安全与更新机制
setValue():主线程即时更新,直接通知观察者。postValue():子线程异步更新,通过ArchTaskExecutor切换到主线程,利用mDataLock保证线程安全。- 仅当观察者处于 STARTED/RESUMED 活跃状态 时才会收到通知,避免后台更新浪费性能。
-
生命周期感知原理
LifecycleBoundObserver监听LifecycleOwner状态,在DESTROYED时自动移除观察者,通过shouldBeActive()判断是否接收数据,实现无内存泄漏的动态订阅。
三、Room 核心总结
-
协程支持与线程调度
- 在 DAO 中定义
suspend函数,Room 编译时生成后台线程代码(默认使用Dispatchers.IO),避免阻塞主线程。 - 可通过
@Query(workerThread = true)或自定义CoroutineDispatcher调整线程池。
- 在 DAO 中定义
-
数据库升级与 Migration
- 通过
@Database(version = X)指定版本,升级时需提供Migration类实现逐版本 SQL 迁移逻辑(如添加字段、修改表结构)。 - 最佳实践:避免跨版本升级(如 1→3),复杂场景可使用
fallbackToDestructiveMigration重置数据库。
- 通过
-
性能优化手段
- 索引:通过
@Index注解提升查询效率,生成CREATE INDEX语句。 - 事务:
@Transaction注解保证批量操作原子性,减少多次 IO 开销。 - 批量操作:使用
insertAll()、updateAll()替代单条操作,降低数据库交互次数。
- 索引:通过
四、Navigation 核心总结
-
返回栈与导航配置
NavController维护BackStack(栈结构),通过导航图中action的popUpTo/popUpToInclusive清理栈条目:popUpTo:指定弹出目标 ID,inclusive=true时包含目标自身,避免冗余页面驻留。
-
参数传递与类型安全
- 支持基本类型和
Parcelable对象,通过导航图argument定义参数,编译时通过@NavigationRes校验目标 ID 合法性。 - 复杂对象需实现
Parcelable或使用@TypeConverter(如 Room 实体),确保序列化 / 反序列化安全。
- 支持基本类型和
-
深层链接集成
- 在导航图中配置
deepLink标签,通过NavDeepLinkBuilder解析 URL,匹配目的地并提取参数(如http://example.com/news/{newsId}),提升应用外部访问能力。
- 在导航图中配置
总结对比
| 组件 | 核心目标 | 关键机制 | 最佳实践 |
|---|---|---|---|
| ViewModel | 生命周期感知的数据管理 | ViewModelStore/Factory 机制 | 依赖注入、避免持有 Activity 强引用 |
| LiveData | 生命周期感知的响应式数据更新 | 版本号校验、LifecycleBoundObserver | 区分 setValue/postValue,处理黏性事件 |
| Room | 类型安全的 SQLite ORM | 编译时代码生成、协程支持 | 合理使用 Migration、索引 / 事务优化性能 |
| Navigation | 多页面导航与返回栈管理 | 导航图配置、NavController 栈管理 | 清晰定义 popUpTo 规则 |
