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 规则 |