如何在导航过程中传递复杂对象参数?
解读
国内面试场景下,这道题通常出现在“Jetpack Navigation 深度使用”环节。面试官想确认两点:
- 你是否真的在商业项目里用 Navigation 做过页面跳转,而不是只会
Intent.putExtra; - 面对“复杂对象”这一高频痛点,你能否给出符合 Android 官方推荐、兼顾性能与生命周期的完整方案,而不是简单回答“实现 Parcelable/Serializable”。
如果只说“用 Parcelable 就行”,会被追问内存拷贝、Bundle size 限制、事务回退栈恢复等问题;如果直接祭出“全局静态 Map 缓存”,又会被质疑内存泄漏与进程回收。因此,答案必须体现“官方化 + 国内落地 + 极限场景兜底”。
知识点
- Jetpack Navigation 参数机制
- 安全 Args 插件仅支持基础类型 + Parcelable/Serializable + ArrayList
- Bundle 有 1 MB 硬限制(Binder 1 MB - 其他事务开销),且跨进程会二次拷贝
- Parcelable 与 Serializable 差异
- Parcelable 性能高 10× 以上,但手写模板代码多;@Parcelize 需 kotlin-android-extensions 插件,国内部分项目已禁用
- Navigation 回退栈重建
- 进程被系统回收后,Navigation 会通过反射重新创建 Fragment,Bundle 中的 Parcelable 会重新反序列化;若类结构变更,会抛 BadParcelableException
- SavedStateHandle
- 本质也是 Bundle,但生命周期与 ViewModel 绑定,可跨配置重建存活
- 共享 ViewModel 模式
- 由 Activity 或 Navigation 父图提供 ViewModelStoreOwner,目标 Fragment 通过
by activityViewModels()或by navGraphViewModels(R.id.xxx)获取同一实例
- 由 Activity 或 Navigation 父图提供 ViewModelStoreOwner,目标 Fragment 通过
- 内存缓存兜底
- 国内厂商 ROM 杀进程激进,Application 被回收后静态变量清零,需结合 MMKV/Room 做落盘
- 安全合规
- 若复杂对象含用户隐私,禁止放 Bundle 明文,需先加密(Tink/Keystore)再序列化
答案
在商业项目里,我们按“对象大小 & 是否含隐私”做三级路由:
-
轻量级(< 100 KB 且不含隐私):
使用 Navigation Safe Args,对象实现 Parcelable,通过gradle插件生成Directions类,直接作为参数传递。
代码示例:@Parcelize data class SkuInfo(val skuId: String, val price: Long) : Parcelable val direction = DetailFragmentDirections.actionToDetail(skuInfo) findNavController().navigate(direction)并在目标 Fragment 用
val skuInfo by navArgs<DetailFragmentArgs>()取值。
优点:官方推荐、类型安全、自动回退栈恢复;缺点:Bundle 1 MB 红线。 -
中等量级(100 KB–500 KB 或含隐私):
采用“共享 ViewModel + SavedStateHandle”双保险。- 在 navGraph 层面定义
ViewModel:
class SharedSkuViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { val skuDetail: MutableLiveData<SkuDetail> = savedStateHandle.getLiveData("key") }- 源 Fragment 通过
by navGraphViewModels(R.id.mobile_navigation)注入并写入数据; - 目标 Fragment 同样注入,直接观察
skuDetail。
这样既避开 Bundle 大小限制,又能在配置重建后通过 SavedStateHandle 自动恢复。
- 在 navGraph 层面定义
-
超大/敏感对象(> 500 KB 或含加密图片流):
先落盘到应用私有目录,再传 URI。- 使用 Room 写入对象(已做 SQLCipher 加密),返回
content://URI(FileProvider); - 通过 Safe Args 只传 URI 字符串与对称加密密钥的别名(密钥存 Keystore,Cipher 后走 Tink);
- 目标 Fragment 通过 URI 重新查询并反序列化。
进程被回收后,Room 数据仍在,URI 通过 SavedStateHandle 恢复,可做到“0 数据丢失”。
- 使用 Room 写入对象(已做 SQLCipher 加密),返回
补充兜底:
- 所有自定义 Parcelable 都加
serialVersionUID与@RawValue注解,防止类结构升级导致反序列化崩溃; - 打开开发者选项“不保留活动”做 monkey 测试,验证回退栈重建无 BadParcelableException;
- 上线前用 Google Play Pre-launch + 国内云测做 1 MB Bundle 超限报警,确保最大对象压缩后 < 500 KB。
拓展思考
- 多端复用:如果同一套代码要跑在车载 Android Automotive 上,Binder 缓冲区更小(通常 512 KB),需要把“共享 ViewModel”降级为“磁盘缓存 + URI”模式,否则导航过程会抛 TransactionTooLargeException。
- 隐私沙盒:Android 13 后限制了后台启动 Intent 携带数据大小,前台导航虽不受限,但建议把敏感字段提前存入 EncryptedSharedPreferences,只传 key,目标页再读取,避免被抓包。
- 性能极限:在 5G 弱网场景,用户可能频繁切前后台导致多次重建,可把 Parcelable 对象用 ProtoBuf 序列化后转 ByteArray,再压缩(Zstd)存 MMKV,读取耗时 < 2 ms,比传统 Bundle 快 30%。
- 可测试性:共享 ViewModel 方案要写 instrumentation test 时,可用
FragmentScenario并手动传入SavedStateHandle的 mock 数据,验证重建后数据一致性,避免测试绿但线上崩。