当前位置: 首页 > news >正文

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 核心总结

  1. 跨 Fragment 数据共享

    • 作用域:由 ViewModelStoreOwner 决定,通过 Activity 作为 Owner 可实现跨 Fragment 共享(作用域为 Activity 生命周期),若用 Fragment 自身作为 Owner 则作用域为 Fragment 生命周期。
    • 原理:Activity 的 ViewModelStore 在配置变更时通过 NonConfigurationInstance 保存,避免 ViewModel 重建。
  2. 生命周期与内存泄漏

    • onCleared() 在 Owner 永久销毁(非配置变更)时调用,需在此清理异步任务(如取消 viewModelScope 协程)、释放资源。
    • 避坑:避免在 ViewModel 中持有 Activity 强引用,改用 getApplication() 获取上下文。
  3. 自定义 Factory 与依赖注入

    • 实现 ViewModelProvider.Factory 可向 ViewModel 传递参数(如 Repository、数据库实例),是手动依赖注入的基础,配合 Hilt 可实现全自动依赖管理。

二、LiveData 核心总结

  1. 黏性事件与非黏性实现

    • 原因:通过版本号 mVersion 机制,新订阅者因初始版本号为 -1,必然触发最新数据回调。
    • 非黏性方案:使用 observeForever() 手动管理订阅,或封装 SingleLiveEvent(单次触发)、结合 MediatorLiveData 过滤旧数据。
  2. 线程安全与更新机制

    • setValue():主线程即时更新,直接通知观察者。
    • postValue():子线程异步更新,通过 ArchTaskExecutor 切换到主线程,利用 mDataLock 保证线程安全。
    • 仅当观察者处于 STARTED/RESUMED 活跃状态 时才会收到通知,避免后台更新浪费性能。
  3. 生命周期感知原理

    • LifecycleBoundObserver 监听 LifecycleOwner 状态,在 DESTROYED 时自动移除观察者,通过 shouldBeActive() 判断是否接收数据,实现无内存泄漏的动态订阅。

三、Room 核心总结

  1. 协程支持与线程调度

    • 在 DAO 中定义 suspend 函数,Room 编译时生成后台线程代码(默认使用 Dispatchers.IO),避免阻塞主线程。
    • 可通过 @Query(workerThread = true) 或自定义 CoroutineDispatcher 调整线程池。
  2. 数据库升级与 Migration

    • 通过 @Database(version = X) 指定版本,升级时需提供 Migration 类实现逐版本 SQL 迁移逻辑(如添加字段、修改表结构)。
    • 最佳实践:避免跨版本升级(如 1→3),复杂场景可使用 fallbackToDestructiveMigration 重置数据库。
  3. 性能优化手段

    • 索引:通过 @Index 注解提升查询效率,生成 CREATE INDEX 语句。
    • 事务@Transaction 注解保证批量操作原子性,减少多次 IO 开销。
    • 批量操作:使用 insertAll()updateAll() 替代单条操作,降低数据库交互次数。

四、Navigation 核心总结

  1. 返回栈与导航配置

    • NavController 维护 BackStack(栈结构),通过导航图中 action 的 popUpTo/popUpToInclusive 清理栈条目:
      • popUpTo:指定弹出目标 ID,inclusive=true 时包含目标自身,避免冗余页面驻留。
  2. 参数传递与类型安全

    • 支持基本类型和 Parcelable 对象,通过导航图 argument 定义参数,编译时通过 @NavigationRes 校验目标 ID 合法性。
    • 复杂对象需实现 Parcelable 或使用 @TypeConverter(如 Room 实体),确保序列化 / 反序列化安全。
  3. 深层链接集成

    • 在导航图中配置 deepLink 标签,通过 NavDeepLinkBuilder 解析 URL,匹配目的地并提取参数(如 http://example.com/news/{newsId}),提升应用外部访问能力。

总结对比

组件核心目标关键机制最佳实践
ViewModel生命周期感知的数据管理ViewModelStore/Factory 机制依赖注入、避免持有 Activity 强引用
LiveData生命周期感知的响应式数据更新版本号校验、LifecycleBoundObserver区分 setValue/postValue,处理黏性事件
Room类型安全的 SQLite ORM编译时代码生成、协程支持合理使用 Migration、索引 / 事务优化性能
Navigation多页面导航与返回栈管理导航图配置、NavController 栈管理清晰定义 popUpTo 规则

相关文章:

  • 2505ahk,wmi学习
  • 本地服务验证-仙盟创梦IDE-智能编程,编程自动备份+编程审计
  • Redis 主从复制部署
  • 生成多个密钥对ssh的key
  • 当MCP撞进云宇宙:多芯片封装如何重构云计算的“芯“未来?
  • 数字智慧方案5857丨智慧机场解决方案与应用(53页PPT)(文末有下载方式)
  • nextTick的作用
  • flowable 使用流程服务
  • 【STM32】定时器的外部时钟模式
  • 如何提升自我情绪管理的能力?
  • 沥青路面裂缝的目标检测与图像分类任务
  • [更新完毕]2025五一杯C题五一杯数学建模思路代码文章教学:社交媒体平台用户分析问题
  • 【Linux】基础指令(2)
  • 红鸟3D互动系统棋类源码一键部署教程(含多个打包版本与功能解构)
  • PowerBI实现点击空白处隐藏弹窗(详细教程)
  • NVIDIA NPP 库入门
  • MySQL初阶:数据库基础,数据库和表操作,数据库中的数据类型
  • STM32MP157开发板设置静态IP地址
  • Windows配置grpc
  • mescroll.js 是在 H5端 运行的下拉刷新和上拉加载插件
  • 李在明涉嫌违反《公职选举法》案将于15日进行首次重审公审
  • 山东一景区怕游客赶不到海撒三千斤蛤蜊:给游客提供情绪价值
  • 范宇任上海宝山区副区长
  • 在差异中建共鸣,《20世纪美国文学思想研究》丛书出版
  • 媒体:每一个“被偷走的人生”,都该得到公道和正义
  • 独家丨申万宏源研究所将迎来新所长:首席策略分析师王胜升任