如何在导航过程中传递复杂对象参数?

解读

国内面试场景下,这道题通常出现在“Jetpack Navigation 深度使用”环节。面试官想确认两点:

  1. 你是否真的在商业项目里用 Navigation 做过页面跳转,而不是只会 Intent.putExtra
  2. 面对“复杂对象”这一高频痛点,你能否给出符合 Android 官方推荐、兼顾性能与生命周期的完整方案,而不是简单回答“实现 Parcelable/Serializable”。
    如果只说“用 Parcelable 就行”,会被追问内存拷贝、Bundle size 限制、事务回退栈恢复等问题;如果直接祭出“全局静态 Map 缓存”,又会被质疑内存泄漏与进程回收。因此,答案必须体现“官方化 + 国内落地 + 极限场景兜底”。

知识点

  1. Jetpack Navigation 参数机制
    • 安全 Args 插件仅支持基础类型 + Parcelable/Serializable + ArrayList
    • Bundle 有 1 MB 硬限制(Binder 1 MB - 其他事务开销),且跨进程会二次拷贝
  2. Parcelable 与 Serializable 差异
    • Parcelable 性能高 10× 以上,但手写模板代码多;@Parcelize 需 kotlin-android-extensions 插件,国内部分项目已禁用
  3. Navigation 回退栈重建
    • 进程被系统回收后,Navigation 会通过反射重新创建 Fragment,Bundle 中的 Parcelable 会重新反序列化;若类结构变更,会抛 BadParcelableException
  4. SavedStateHandle
    • 本质也是 Bundle,但生命周期与 ViewModel 绑定,可跨配置重建存活
  5. 共享 ViewModel 模式
    • 由 Activity 或 Navigation 父图提供 ViewModelStoreOwner,目标 Fragment 通过 by activityViewModels()by navGraphViewModels(R.id.xxx) 获取同一实例
  6. 内存缓存兜底
    • 国内厂商 ROM 杀进程激进,Application 被回收后静态变量清零,需结合 MMKV/Room 做落盘
  7. 安全合规
    • 若复杂对象含用户隐私,禁止放 Bundle 明文,需先加密(Tink/Keystore)再序列化

答案

在商业项目里,我们按“对象大小 & 是否含隐私”做三级路由:

  1. 轻量级(< 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 红线。

  2. 中等量级(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 自动恢复。
  3. 超大/敏感对象(> 500 KB 或含加密图片流):
    先落盘到应用私有目录,再传 URI。

    • 使用 Room 写入对象(已做 SQLCipher 加密),返回 content:// URI(FileProvider);
    • 通过 Safe Args 只传 URI 字符串与对称加密密钥的别名(密钥存 Keystore,Cipher 后走 Tink);
    • 目标 Fragment 通过 URI 重新查询并反序列化。
      进程被回收后,Room 数据仍在,URI 通过 SavedStateHandle 恢复,可做到“0 数据丢失”。

补充兜底:

  • 所有自定义 Parcelable 都加 serialVersionUID@RawValue 注解,防止类结构升级导致反序列化崩溃;
  • 打开开发者选项“不保留活动”做 monkey 测试,验证回退栈重建无 BadParcelableException;
  • 上线前用 Google Play Pre-launch + 国内云测做 1 MB Bundle 超限报警,确保最大对象压缩后 < 500 KB。

拓展思考

  1. 多端复用:如果同一套代码要跑在车载 Android Automotive 上,Binder 缓冲区更小(通常 512 KB),需要把“共享 ViewModel”降级为“磁盘缓存 + URI”模式,否则导航过程会抛 TransactionTooLargeException。
  2. 隐私沙盒:Android 13 后限制了后台启动 Intent 携带数据大小,前台导航虽不受限,但建议把敏感字段提前存入 EncryptedSharedPreferences,只传 key,目标页再读取,避免被抓包。
  3. 性能极限:在 5G 弱网场景,用户可能频繁切前后台导致多次重建,可把 Parcelable 对象用 ProtoBuf 序列化后转 ByteArray,再压缩(Zstd)存 MMKV,读取耗时 < 2 ms,比传统 Bundle 快 30%。
  4. 可测试性:共享 ViewModel 方案要写 instrumentation test 时,可用 FragmentScenario 并手动传入 SavedStateHandle 的 mock 数据,验证重建后数据一致性,避免测试绿但线上崩。