Jetpack 架构组件 ★
你的重点短板. Jetpack 是应用开发日常, 中级面试必问 ViewModel/LiveData/Room/Hilt 原理. 架构组织与选型见 MVVM 与 MVI, 完整落地见 App 架构落地案例.
一, ViewModel
- 作用: 持有 UI 相关数据, 在配置变更 (旋转) 时存活, 与 UI 解耦.
- 存活原理: 对外契约是同一
ViewModelStoreOwner在配置变更后可取回同一个 ViewModel, 直到该 owner 真正结束才调用onCleared(). 实现层面可理解为ViewModelStore被保留并在重建后重新关联, 历史实现中会经过 Activity 的 NonConfigurationInstance 保留链路; 面试中不要把某个内部方法名当成稳定 API 保证. - 不要持有 View/Context 引用 (会泄漏); 需要 Context 用 AndroidViewModel (持 Application).
SavedStateHandle: 在进程被杀重建后恢复关键数据 (配合 savedState).viewModelScope: 绑定 ViewModel 的协程作用域, onCleared 时取消.
二, Lifecycle
- LifecycleOwner: Activity/Fragment 实现它, 暴露 Lifecycle.
- LifecycleObserver: 观察生命周期事件, 把生命周期相关逻辑 (如开始 / 停止定位) 从组件中解耦出去.
- 现代用
DefaultLifecycleObserver或lifecycleScope.launch { repeatOnLifecycle(STATE) { } }.
LifecycleRegistry 状态机与派发
LifecycleRegistry 将 Owner 的生命周期表示为状态机, 对外重点是状态和事件的对应语义, 不应依赖其某个内部观察者集合或遍历实现. 常见状态为 INITIALIZED, CREATED, STARTED, RESUMED 和终态 DESTROYED.
| 状态方向 | 事件 | Owner 状态变化 |
|---|---|---|
| 上行 | ON_CREATE | INITIALIZED -> CREATED |
| 上行 | ON_START | CREATED -> STARTED |
| 上行 | ON_RESUME | STARTED -> RESUMED |
| 下行 | ON_PAUSE | RESUMED -> STARTED |
| 下行 | ON_STOP | STARTED -> CREATED |
| 下行 | ON_DESTROY | CREATED -> DESTROYED |
新观察者加入后会被同步到 Owner 当前状态: 例如 Owner 已处于 STARTED, 新观察者会依序补收上行事件直到 STARTED, 而不是只等待下一次事件. Owner 上行时, 已注册观察者按前向顺序接收事件; Owner 回退时, 以相反顺序派发下行事件, 使后建立的依赖先释放. 回调中新增或移除观察者, 或再次触发状态变化属于重入情形; Registry 会以重新同步为目标处理这类变化, 因此观察者回调应保持短小, 幂等, 不应假设某次遍历期间的观察者集合或回调次数是跨版本固定的实现细节.
DESTROYED 是终态. Owner 销毁后, 与其绑定的观察关系和资源应清理, 不应期待该 Owner 或其观察者再回到 CREATED/STARTED 并恢复使用. 需要重新展示页面或重新订阅时, 应使用新建的 Owner 和新的观察关系.
使用边界:
- Activity 自身的长期 UI 资源可绑定
this的 Lifecycle, 但仍应在不可见或停止时按业务停止昂贵工作. - Fragment 的 View, View Binding, RecyclerView adapter 和收集 UI 状态应绑定
viewLifecycleOwner, 而不是 Fragment 生命周期, 避免 View 销毁后继续访问旧 View. - 定位, 相机预览, 传感器和网络订阅等资源按可见性或交互状态启停; 观察者只协调开始 / 停止, 资源本身仍需处理权限, 失败, 取消和释放.
- Flow 收集使用
viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { ... } }; 进入目标状态时启动子协程, 低于该状态时取消, View 销毁时随 owner 清理. 它适合可重新收集的 UI 流, 不替代需要持久化的业务状态或跨进程任务机制.
三, LiveData
- 生命周期感知的可观察数据持有者: 只在活跃状态 (STARTED/RESUMED) 通知观察者, 组件销毁自动移除观察, 避免泄漏和崩溃.
setValue(主线程)/postValue(任意线程).- 粘性语义: 新观察者会立即收到最新状态. 不要用 SingleLiveEvent 或 SharedFlow (replay=0) 作为统一 “事件修复”; 必须处理的业务结果应归约为可恢复 UI state, 纯 UI 局部动作由 UI 直接处理.
- MediatorLiveData: 合并多个源.
- 现代项目通常用 StateFlow 表达持久状态; SharedFlow 是否适用取决于允许丢失, 重放和多订阅语义, 不能与 LiveData 做机械的一对一替换.
四, Room
- SQLite 之上的 ORM, 编译期校验 SQL.
- 三要素:
@Entity(表),@Dao(数据访问接口),@Database(数据库持有者). - 支持 协程 suspend 和 Flow 返回 (数据变化自动推送).
- 数据库迁移 Migration: 版本升级写 Migration, 否则崩溃; 开发期可
fallbackToDestructiveMigration(清库). - 查询返回 Flow 时, 表数据变化会自动重新发射, 天然支持响应式 UI.
五, Navigation
- 定位: 单 Activity 多 Fragment/Composable 架构的导航框架, 把 “目的地, 参数, Action, Deep Link, 回退栈” 集中声明, 减少手写 FragmentTransaction/Intent flag 的分散逻辑.
- 对象关系:
NavHost: 承载导航内容的容器, Fragment 场景常见NavHostFragment, Compose 场景是NavHostComposable.NavController: 执行navigate/popBackStack的控制器, 维护当前目的地和 back stack.NavGraph: 目的地和跳转关系的图, 可来自 XML, 也可在 Compose 中用 Kotlin DSL 声明.NavBackStackEntry: 回退栈条目, 携带 arguments, Lifecycle, SavedStateHandle, 也可作为 ViewModel 作用域边界.
- 导航调用流程:
View 点击/Intent → 调用 NavController.navigate(route/action) → 按 NavGraph 匹配目的地与参数 → 创建或恢复目的地 → 新 NavBackStackEntry 入栈 → 目标 Fragment/Composable 渲染 → 返回时 popBackStack 出栈并恢复上一 entry. - 回退栈 API:
navigate()入栈新目的地; 可用popUpTo清理中间页面, 登录后清空登录流常用.popBackStack()返回上一目的地; 指定 destination/route 时可弹到某个节点.launchSingleTop避免重复点击把同一目的地压多份.
- Deep Link: 可以在图中声明 URI/Action/MIME 匹配. 外部 Intent 进入后由 Navigation 匹配目标目的地并补齐必要的 back stack; 面试要强调参数校验和未登录拦截, 不要只说 “配置一个 deepLink”.
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(onOpenDetail = { id -> navController.navigate("detail/$id") })
}
composable(
route = "detail/{id}",
arguments = listOf(navArgument("id") { type = NavType.StringType }),
) { entry ->
DetailScreen(id = entry.arguments?.getString("id") ?: return@composable)
}
}
}
类型安全路由 (Navigation 2.8+): Navigation Compose / Kotlin DSL 内置, 官方视作 XML 图时代 Safe Args 的等价物. 用 @Serializable 标注路由类型 (object Home / data class Profile(val id: String)), composable<T>() 声明目的地, navigate(Profile(id)) 跳转, toRoute<T>() 取参, 免手写字符串 route 与 navArgument.
- 弃字符串 route 的原因: 拼写错误无编译期校验, 参数名 / 类型易错且运行期才暴露; 新代码优先类型安全, 字符串 route 退居旧项目 / 简单跳转.
常见错误:
- 在多个页面散落手写跳转字符串, 导致参数名 / route 不一致; 可用常量, 封装 route builder 或 Safe Args 降低风险.
- 登录, 首页, 详情都压入同一栈后不清理, 返回路径混乱; 需要明确
popUpTo和inclusive. - 把未建模的
navigate=true永久留在 State 会造成重复导航. 用户点击产生的导航由 UI 直接执行; 若导航依赖业务完成结果, ViewModel 先写入可消费 / 可清除的状态, UI 观察后导航并回调确认. 不使用 SharedFlow (replay=0) 假装获得可靠投递. - Deep Link 只做跳转不做鉴权和参数兜底, 容易打开非法状态页面.
面试答题结构: 先说 “NavGraph + NavHost + NavController” 三件套 → 再讲 back stack entry 的入栈/出栈流程 → 补 Compose/XML 两种写法 → 最后说 deep link, 参数安全, 重复导航与登录栈清理.
六, Hilt (依赖注入)
- 基于 Dagger 的 Android 专用 DI 封装, 大幅减少模板代码.
- 核心注解:
@HiltAndroidApp(Application),@AndroidEntryPoint(注入到组件),@Inject(构造注入),@Module+@Provides/@Binds(提供无法构造注入的依赖),@HiltViewModel(注入 ViewModel). - 作用域:
@Singleton,@ActivityScoped,@ViewModelScoped等, 控制实例生命周期. - Hilt vs Dagger: Hilt 预定义了 Android 组件的标准注入点和组件层级, 开箱即用; Dagger 更灵活但配置繁琐.
- DI 的意义: 解耦, 可测试 (可替换 mock 实现), 统一管理对象创建.
Room + Hilt + ViewModel + DataStore 最小链路
上下文片段: 此最小工程片段需 Room, Hilt, DataStore Preferences, Lifecycle ViewModel 和 KSP/KAPT 依赖; Application 标注 @HiltAndroidApp, Activity/Fragment 标注 @AndroidEntryPoint. 省略 imports, Gradle 配置与 UI 收集. 插入用户后 observeUsers() 预期发射包含该用户的列表; 从数据库版本 1 升至 2 时执行列新增 migration, 而非清库.
@Entity(tableName = "users") data class UserEntity(@PrimaryKey val id: String, val name: String)
@Dao interface UserDao { @Query("SELECT * FROM users") fun observeUsers(): Flow<List<UserEntity>>; @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(user: UserEntity) }
@Database(entities = [UserEntity::class], version = 2) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE users ADD COLUMN name TEXT NOT NULL DEFAULT ''") } }
@Module @InstallIn(SingletonComponent::class) object AppModule {
@Provides @Singleton fun db(@ApplicationContext context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "app.db").addMigrations(MIGRATION_1_2).build()
@Provides fun dao(db: AppDatabase): UserDao = db.userDao()
}
val Context.dataStore by preferencesDataStore("settings")
@HiltViewModel class UsersViewModel @Inject constructor(dao: UserDao, @ApplicationContext context: Context, private val savedState: SavedStateHandle) : ViewModel() {
val users = dao.observeUsers().stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
val darkMode = context.dataStore.data.map { it[booleanPreferencesKey("dark_mode")] ?: false }
}
DataStore 适合保存小型偏好而不是用户表, 上例已将其读取实际接入 ViewModel. Hilt 层级是 SingletonComponent -> ActivityRetainedComponent -> (ViewModelComponent, ActivityComponent -> FragmentComponent): ViewModelComponent 与 ActivityComponent 是并列分支, 不能把前者说成后者的父/子组件. 对象 scope 必须与其持有资源的生命周期匹配. 迁移失败的症状是升级崩溃或 schema 校验失败, 证据是 migration test/升级安装日志, 修复是补齐从旧版本到新版本的迁移并验证保留旧数据.
上下文片段 (RemoteMediator 写库): db 是同一 Room 数据库, productDao/remoteKeyDao 分别操作列表和分页 key; 网络页已成功映射为 entities. 刷新时必须在一个 withTransaction 中同时清旧数据, 写新数据和更新 remote keys, 预期任一步抛异常时整笔事务回滚, UI 继续观察旧的一致快照而不会看到 “新列表配旧 key”.
db.withTransaction {
if (loadType == LoadType.REFRESH) {
remoteKeyDao.clear()
productDao.clear()
}
productDao.insertAll(page.products.map(::ProductEntity))
remoteKeyDao.insertAll(page.products.map { RemoteKey(it.id, page.nextKey) })
}
七, DataStore / WorkManager / Paging
- SharedPreferences 一致性边界: SP 会维护进程内缓存, 同一进程内的常规读写由实现同步协调, 但这不等于它提供跨进程事务或多进程一致性保证.
commit()会在调用线程同步写磁盘并返回成功与否, 因此不应放在主线程热路径;apply()会先更新内存并异步调度落盘, 不返回写入结果, 进程异常终止或写入失败时不能把它当成已持久化确认. 多个进程同时访问, 旧的多进程模式或非原子修改会带来陈旧读, 覆盖和文件损坏风险, 不应将 SP 作为跨进程共享状态通道. - 迁移方向与版本边界: 新代码优先使用 DataStore 保存小型偏好, 其中 Preferences DataStore 保留键值模型, Proto DataStore 提供 schema 与类型约束; 通过迁移在首次访问时将已知 SP 键复制到目标存储并验证迁移后行为. 默认的单进程
DataStoreFactory/preferencesDataStore不是自动多进程方案. DataStore 1.1.0+ 的MultiProcessDataStoreFactory才用于多个进程访问同一文件; 每个进程对该文件只能创建一个实例, 且所有进程必须使用多进程工厂, 不可与普通实现混用同一路径. 不需要多进程存储时, 跨进程数据仍应由带明确并发与访问控制契约的 Provider, 数据库或服务接口承担. 保留 SP 的旧版本应先定义数据归属, 迁移窗口和回退读取策略, 不让两端长期双写. - DataStore: 替代 SharedPreferences. 基于协程 + Flow, 异步, 事务安全; 正确使用异步 API 可降低常见主线程磁盘 IO 风险, 但阻塞式误用, 迁移或上游工作仍可能造成卡顿. 两种: Preferences DataStore (键值), Proto DataStore (类型安全). SP 的
apply()会先更新内存再异步持久化, 但待落盘写入及其与生命周期的交互仍可能导致卡顿;getXXX也可能带来主线程 IO 风险, 且无类型安全. - WorkManager: 用于可延迟, 可持久化且期望最终执行的后台任务, 即使 App 退出 / 重启后也可由系统重新调度. 执行时间不精确, 强行停止应用和系统约束等是边界. 支持约束 (网络, 充电), 链式, 周期任务. 底层 API 23+ 用 JobScheduler, 低版本历史上用 GcmNetworkManager (已弃用), 现退化为 AlarmManager 兜底与重启重调度. 适合上传日志, 同步数据.
- Paging3: 分页加载大列表, 核心是
PagingSource → Pager → Flow<PagingData<T>> → UI Adapter/Compose.PagingSource<Key, Value>: 定义load(params)如何按页加载数据, 返回LoadResult.Page/Error;getRefreshKey()决定刷新时从哪里继续.Pager: 接收PagingConfig和pagingSourceFactory, 把分页请求包装成Flow<PagingData<T>>.PagingData: 一次分页会话的数据流, UI 侧通过collectAsLazyPagingItems()或PagingDataAdapter.submitData()消费.RemoteMediator: 网络 + 本地数据库场景的协调器, 负责判断何时从网络拉新页, 写入 Room, UI 实际从 Room 的 PagingSource 读取, 形成单一数据源.
class UserPagingSource(private val api: UserApi) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> = try {
val page = params.key ?: 1
val users = api.users(page = page, size = params.loadSize)
LoadResult.Page(
data = users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (users.isEmpty()) null else page + 1,
)
} catch (t: Throwable) {
LoadResult.Error(t)
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? =
state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
}
}
val users: Flow<PagingData<User>> = Pager(
config = PagingConfig(pageSize = 20, prefetchDistance = 5),
pagingSourceFactory = { UserPagingSource(api) },
).flow.cachedIn(viewModelScope)
RemoteMediator 流程: UI 触发刷新/滑到底 → Paging 判断需要加载 → RemoteMediator.load(REFRESH/PREPEND/APPEND) 请求网络 → 事务写入 Room + remote keys → Room 的 PagingSource 失效并重新发射 → UI 收到新的 PagingData. 这种方式适合离线缓存, 列表恢复, 网络与本地一致性要求高的页面.
UI 集成要点:
- RecyclerView 用
PagingDataAdapter+LoadStateAdapter展示刷新, 加载更多, 错误重试. - Compose 用
collectAsLazyPagingItems()和LazyColumn.items(count); 根据loadState.refresh/append渲染首屏 loading, 尾部 loading, 错误重试. - 在 ViewModel 中
cachedIn(viewModelScope), 避免旋转屏幕后重新创建分页流导致重复请求.
常见错误:
PagingSource同时读网络和本地且没有一致性策略, 刷新 / 翻页状态难维护; 复杂缓存场景应考虑RemoteMediator + Room.- 忘记实现合理的
getRefreshKey, 刷新后列表跳到错误位置. - 不处理
LoadState, 用户看不到加载, 错误, 重试状态. - 每次 UI 重组 / 重建都新建 Pager, 导致重复加载; 应让分页流由 ViewModel 持有并缓存.
面试答题结构: 先说 “PagingSource 负责加载一页, Pager 产出 PagingData Flow, UI 订阅渲染” → 再补 “RemoteMediator 用于网络 + 数据库单一数据源” → 最后讲 cachedIn, LoadState, 刷新 key, 错误重试这些工程坑.
高频面试题
Q1: ViewModel 为什么能在旋转屏幕后存活? 旋转是配置变更, Activity/Fragment 会重建, 但同一 ViewModelStoreOwner 的 ViewModelStore 会在配置变更后重新关联, 因此可取回同一个 ViewModel 实例. 历史实现可从 NonConfigurationInstance 保留链路理解, 但对外稳定契约是 “配置变更存活, owner 真正结束时 onCleared”, 不要依赖内部方法名.
Q2: ViewModel 能持有 Context 吗? 不能持有 Activity/View Context (泄漏).需要 Application Context 时用 AndroidViewModel. 原则: ViewModel 不感知 UI.
Q3: LiveData 的粘性语义对 UI 事件有什么影响? 新观察者会收到最新状态, 因而不适合直接承载未设计消费语义的动作. 不推荐用 SingleLiveEvent, Event wrapper 或 SharedFlow (replay=0) 作为统一补丁: 必须处理的业务结果进入可恢复 UI state, UI 处理后回调确认; 用户本地导航由 UI 直接触发; 允许丢失的瞬时效果才使用事件流.
Q4: LiveData 和 StateFlow 怎么选? 新项目优先 StateFlow (纯 Kotlin, 操作符丰富, 与协程统一), 需配 repeatOnLifecycle 做生命周期安全收集; 老项目 / 简单场景 LiveData 自带生命周期感知更省事.
Q5: Room 相比直接用 SQLite 的优势? 编译期校验 SQL, 减少样板代码, 对象映射, 原生支持协程和 Flow (数据变化自动推送), 迁移管理.
Q6: 为什么用 DataStore 替代 SharedPreferences?
SP 有主线程 IO 风险, apply() 会先更新内存再异步持久化, 但待落盘写入及其与生命周期的交互仍可能导致卡顿, 且无类型安全, 无错误信号. DataStore 基于协程 + Flow, 异步, 事务, 类型安全 (Proto), 但仍要避免阻塞式误用, 迁移或上游工作造成卡顿.
Q7: Hilt 和 Dagger 关系? DI 解决什么问题? Hilt 是 Dagger 的 Android 封装, 预置组件层级和注入点. DI 解决对象创建与依赖管理: 解耦, 便于替换实现做单元测试, 避免手动 new 的耦合.
Q8: 什么任务适合 WorkManager? 和协程 / Service 怎么区分? 适合 “可延迟, 可持久化且期望最终执行” 的后台任务 (日志上传, 数据同步), 即使进程被杀 / 重启也可由系统重新调度; 不保证精确时间, 强行停止应用和系统约束是边界. 即时 UI 相关异步用协程; 需要持续运行的用前台 Service.
Q9: Navigation 的回退栈怎么理解? NavController 根据 NavGraph 导航时会把目的地包装成 NavBackStackEntry 入栈, entry 携带参数, 生命周期和状态. 返回时 popBackStack 弹出当前 entry 并恢复上一个; 登录流, 首页清栈要用 popUpTo/launchSingleTop 控制, 避免重复页面和错误返回路径.
Q10: Paging3 的三件套是什么? RemoteMediator 解决什么? PagingSource 负责按 key 加载一页数据, Pager 把它包装成 PagingData 流, UI 用 PagingDataAdapter 或 Compose LazyPagingItems 渲染. RemoteMediator 用于网络 + 本地数据库: 网络结果写入 Room, UI 从 Room 读, 实现离线缓存和单一数据源.
易错点 / 追问
- ViewModel 持有 Activity/View/Context 引用是泄漏: 需要 Application Context 时用
AndroidViewModel, 页面级资源一律绑定viewLifecycleOwner. - 混用
viewLifecycleOwner与 Fragment 的lifecycleOwner: View 销毁后仍访问旧 View; Flow 收集用repeatOnLifecycle. - 用 SingleLiveEvent / SharedFlow (replay=0) 当统一 “事件修复”: 无 collector 时丢值; 必须处理的业务结果归约为可恢复 UI state.
- Room 升级不写 Migration: 升级后 schema 校验失败直接崩溃;
fallbackToDestructiveMigration只清库, 不能当发布方案. - Navigation 字符串 route 散落各处且参数无编译期校验, 登录后不清栈导致返回路径混乱; 新代码优先类型安全路由 +
popUpTo. - Hilt scope 与资源生命周期不匹配: 过长泄漏, 过短重复创建;
ViewModelComponent与ActivityComponent是并列分支, 不是父子. - 把 SP
apply()当已持久化确认, 或用默认 DataStore 工厂跨进程访问: 都不提供跨进程一致性; 多进程须用MultiProcessDataStoreFactory并全进程统一. - Paging 不实现
getRefreshKey/ 不处理 LoadState / 每次重组新建 Pager: 刷新跳位, 用户看不到加载错误态, 重复请求; 分页流应由 ViewModel 持有并cachedIn.
进阶补充: Hilt, Room, WorkManager 与 Compose 生命周期
Hilt 组件层级与作用域
常见层级: SingletonComponent → ActivityRetainedComponent, 其下分为 ViewModelComponent 与 ActivityComponent, 再由 ActivityComponent → FragmentComponent. 作用域要和生命周期匹配, 不要把短生命周期对象注入成单例.
Qualifier 与 Assisted Injection
同类型多个实现用 @Qualifier 区分; 运行时参数可用 assisted injection 或 ViewModel 的 SavedStateHandle.
Room 迁移与 TypeConverter
Room migration 要显式描述 schema 变化, 不能依赖删除重建. 复杂类型用 TypeConverter, 关系查询要注意 N+1 和事务一致性.
collectAsStateWithLifecycle
Compose 收集 Flow 推荐用 lifecycle-aware API, 避免页面不可见时仍持续收集.
WorkManager 约束和退避
WorkManager 适合可延迟, 可持久化且期望最终执行的后台任务. 约束包括网络, 充电, 空闲; 失败可配置 backoff 和 unique work policy, 但执行时间不精确且受系统与强行停止应用等边界影响.
后台任务与状态恢复决策矩阵
| 场景 | 推荐机制 | 进程死亡后的语义 | 关键边界 |
|---|---|---|---|
| 页面内短任务 | 生命周期感知的协程 | 不保证继续, 页面销毁时应取消 | 使用 viewModelScope 或 repeatOnLifecycle, 不要脱离所有权创建全局协程 |
| 配置变更期间保留页面状态 | ViewModel | 同进程配置变更可保留, 进程死亡后旧实例丢失 | 不持有 View 或 Activity Context |
| 进程死亡后恢复少量 UI 状态 | SavedStateHandle | 系统重建 owner 时可恢复已保存的小数据 | 不存大对象和关键业务事实 |
| 可延迟但要求最终可靠执行 | WorkManager | 任务记录可持久化并由系统重新调度 | 约束, 重试, 唯一任务和幂等必须明确 |
| 用户可见的持续工作 | Foreground Service | 进程仍可能被终止, 需要恢复设计 | 必须及时展示通知, 遵守前台服务类型和后台启动限制 |
| 指定时间触发 | AlarmManager | 系统负责触发, 业务状态仍需持久化 | 精确闹钟需要权限和合理业务理由, 到点后的耗时工作通常再交给其他机制 |
选择机制时依次回答五个问题:
- 工作是否必须跨页面, 进程死亡或设备重启继续?
- 用户是否需要实时看到持续执行和通知?
- 是要求精确触发时间, 还是只要求满足约束后最终执行?
- 取消由谁触发, 已完成一半时如何清理或恢复?
- 重试会不会重复扣款, 重复上传或覆盖新数据?
WorkManager 的 Result.retry() 只是请求以后重试, 不等于业务天然可靠. Worker 应使用稳定业务 ID 做幂等, 持久化阶段进度, 为网络和电量设置约束, 区分可重试错误与永久错误, 并为取消实现协作式清理. 周期任务也不是精确定时器, 系统可以为了省电合并和延迟执行.
追问: 为什么 Hilt scope 不能乱用? 因为对象生命周期过长会造成泄漏, 过短会导致重复创建和状态丢失.