Android Activity间通信全解析:Intent、Bundle与数据恢复策略

发布时间:2026/10/2 11:59:39
Android Activity间通信全解析:Intent、Bundle与数据恢复策略
1. 从一次传个参数聊起Activity通信背后到底在解决什么做安卓开发的人应该都有过这种感觉写Activity跳转太简单了new一个IntentstartActivity甩出去就完事。但真到了项目里你会发现通信这两个字比想象中重得多。早期我在一个小工具类App里接手需求页面A要跳转到页面B传一个用户对象B要回传一个修改结果本来想直接new Intent塞进去结果B返回时数据丢了、没回调onActivityResult、build之后一重启页面就崩。那一次排查让我彻底明白Activity之间通信核心难点从来不是API怎么调而是搞清楚数据从哪来、往哪去、生命周期突然中断时谁负责兜底。先给新手一个整体认知。Activity之间的通信按需求可以拆成几类单向传参A启动B顺带把参数带给B。双向交互A启动BB处理完成后再把结果交还A。状态共享多个Activity需要共同访问同一份数据不需要每一次都通过Intent搬运。事件通知某个页面状态变了另一个还没启动的页面或已存在的页面需要感知到。不同需求对应不同的技术方案硬用同一种方式处理所有场景问题迟早会在某个角落爆发。我见过一些项目图省事统一用静态变量传对象。开发期确实爽上线后一压后台就是各种空指针、脏数据、状态错乱。原因是静态变量是进程级别的Activity是会被系统回收重建的你没法保证一个Activity还在栈里的时候那个静态变量一定没被别的页面覆盖过。所以在讲具体方案之前先给你搭一个判断坐标系**传一次数据用Intent传完整来回的结果用Activity Result API多个页面共享同一批数据用ViewModel或持久化要解耦发送方和接收方才考虑事件总线。**后面每一节我都围绕这个坐标系展开。2. Intent传值不只是把数据塞进去那么简单2.1 显式和隐式两种启动方式对应的通信语义Intent是Activity之间通信的基础载体。它分两种显式Intent和隐式Intent。显式Intent目标明确直接指定要启动哪个类val intent Intent(this, DetailActivity::class.java) intent.putExtra(article_id, 1024) startActivity(intent)隐式Intent则不指定具体类而是声明一个动作、数据URI或类型让系统去匹配哪个Activity能处理val intent Intent(Intent.ACTION_VIEW) intent.setData(Uri.parse(app://product/1024)) startActivity(intent)很多新手搞不清为什么要有隐式Intent。实际项目里隐式的意义在于把我要干什么和谁来干解耦。比如一键分享、打开地图、跳转支付结果页你的App只负责发一个动作具体目标由系统或第三方处理。这在模块化、组件化项目中尤其重要。但要注意隐式Intent涉及的IntentFilter配置、uri匹配规则往往是各种诡异Bug的来源后面我会专门讲。2.2 Bundle传递的类型限制远不止基本类型那么简单Intent传值底层靠Bundle。Bundle是个键值对容器支持的默认类型包括基本数据类型及数组、String、CharSequence、Parcelable、Serializable、Bundle本身等。实际开发中大家最常用的组合是val bundle Bundle().apply { putString(title, 通信详解) putInt(page, 10) putParcelable(user, user) // user实现了Parcelable putSerializable(config, config) // config实现了Serializable } intent.putExtras(bundle)取的时候对应关系要严格匹配类型不对会直接抛ClassCastException。这里有个不太起眼但很重要的限制Binder事务缓冲大小。Intent在进程间传递时数据要写入Binder缓冲区这个缓冲区默认大概是1MB不同系统版本有差异实际能用的稳定空间往往只有500KB上下。也就是说你绝对不能把大图、大列表、大JSON塞进Intent。有一次线上反馈用户给文章配了张截图再点分享崩溃率突然上升查到最后就是Bitmap被塞进Intent导致TransactionTooLargeException。正确的做法是传图片路径或URI接收方自己加载。2.3 Serializable vs Parcelable性能与稳定性的选择初学阶段很多人纠结到底实现哪个接口。直接给结论Serializable是Java原生的序列化方案使用反射写起来简单但性能差序列化体积大。Parcelable是Android特有的序列化方案手动编写序列化过程性能高是官方推荐方式。如果你传的是自定义对象且这个对象只在App内部流转优先实现Parcelable。但要注意Parcelable的坑也不少。比如类字段增减后没有更新describeContents和writeToParcel会出现运行时字段错乱内部类、子类未正确实现时反序列化阶段可能抛异常。我自己习惯是对象字段稳定后再实现Parcelable频繁变动的模型先用Bundle拼字段传降低维护成本。从代码角度给大家一个参考一个简单的Parcelable实现长这样data class User( val id: Int, val name: String, val avatar: String, val level: Int ) : Parcelable { constructor(parcel: Parcel) : this( parcel.readInt(), parcel.readString() ?: , parcel.readString() ?: , parcel.readInt() ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeInt(id) parcel.writeString(name) parcel.writeString(avatar) parcel.writeInt(level) } override fun describeContents(): Int 0 companion object CREATOR : Parcelable.CreatorUser { override fun createFromParcel(parcel: Parcel): User User(parcel) override fun newArray(size: Int): ArrayUser? arrayOfNulls(size) } }字段不多的话手写没问题字段一多建议用kotlin-parcelize插件一个Parcelize注解就搞定避免手写不平等的write和read顺序。3. 返回结果从老式回调到Activity Result API的进化3.1 startActivityForResult的问题在哪页面A跳到页面BB处理完要回传数据这是最常见的双向通信需求。老写法是startActivityForResult(intent, REQUEST_CODE) // 在A里重写 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode REQUEST_CODE) { if (resultCode RESULT_OK) { val result data?.getStringExtra(result_key) } } }这套API最大的问题有两个。一是requestCode和回调之间的硬绑定。一旦页面里同时发起多个请求回调里就全是if套if代码可读性和维护性急剧下降。二是回调时机不受控。Activity在极端情况下可能被系统回收重建onActivityResult有可能在onStart之后才回调如果你在onCreate里做了初始化回调到时拿到的是过期状态很容易踩空指针。3.2 registerForActivityResult注册式回调的正确姿势AndroidX里有了Activity Result API彻底改变了这一块的写法。核心思路是预先注册契约启动操作和回调处理绑定在一起class MainActivity : AppCompatActivity() { private val editLauncher registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result - // result.data 就是目标Activity回传的Intent if (result.resultCode RESULT_OK) { val backValue result.data?.getStringExtra(back_key) binding.textResult.text backValue } } fun startEdit() { editLauncher.launch(Intent(this, EditActivity::class.java)) } }关键点在于registerForActivityResult必须在onCreate或onStart之前完成注册因为系统需要保证重建后回调仍然存在。官方还内置了一堆Contract比如TakePicture、GetContent、RequestPermission平时要写的那一大堆onRequestPermissionsResult逻辑都能省掉。3.3 返回数据链路里我踩过的真实坑第一个坑设置返回结果之后忘了finish。B页面写setResult(RESULT_OK, data)然后继续留在栈里用户手动返回时A才收到回调逻辑上没问题但B里的代码可能因为多停留一段时间又改了data的部分字段导致回调数据不是最终状态。正确做法是确认返回后立刻finish。第二个坑launch的Intent用错addFlags。有些同学从跳转页复制代码带了FLAG_ACTIVITY_NEW_TASK或FLAG_ACTIVITY_CLEAR_TOP回来时发现回调直接变null。因为这些flag会改变Activity栈的复用方式返回的target被目标Activity替换了而不是keep在上层结果回调自然走不到。用result API时启动Intent的flags要保持纯粹不要乱加。第三个坑resultCode判断不完整。很多代码只判断RESULT_OK但用户取消操作时resultCode是RESULT_CANCELED默认0data为null不做判空会直接空指针。哪怕是只有一个返回路径的业务也建议统一判空。4. 跳出Intent限制共享ViewModel、全局单例和事件总线的定位边界4.1 全局单例与静态变量能不用就不用除非你懂边界一些项目图省事定义了一个UserManager单例或AppData.currentUser静态变量A页面赋值B页面读取。开发效率确实高但代价是生命周期完全失控。静态变量的存活期是整个进程而Activity的存活期只是用户看到的那个时间窗口。一旦Activity被系统销毁重建后旧数据可能被新数据覆盖或者数据还在但页面拿到的是重建后的新实例两者之间没有直接绑定关系。全局单例也不是绝对不能用它适合放真正全局且进程生命周期内保持稳定的数据比如登录态、设备信息。不适合放某个业务流里的临时状态比如表单的草稿、列表的筛选条件。后者应该用带作用域的方案。4.2 共享ViewModel生命周期作用域的正规军Activity之间通信更推荐用ViewModel。注意区分ViewModelProvider默认的scope是一个Activity所以两个不同Activity拿到的不是同一个实例。但有一个技巧把作用域绑定到Application上实现跨Activity共享val sharedViewModel ViewModelProvider( viewModelStoreOwner activity, factory ViewModelProvider.AndroidViewModelFactory.getInstance(application) )[SharedViewModel::class.java]更常见的做法是在单Activity架构里共享同一个Activity的ViewModel给多个Fragment用。这也是现在Google推荐的页面内通信方式。跨Activity的场景如果两个Activity从属于同一个业务流程可以考虑用ActivityResult 带状态的数据对象组合而不是直接共享一个全局ViewModel。毕竟跨页面的共享数据终究要考虑页面销毁重建后的恢复问题。4.3 事件总线最后的手段用了就必须处理生命周期很多人喜欢引入EventBus或LiveDataBus来解耦通信。它们确实能让代码看起来干净不用写那么多Intent和接口但代价是发送方和接收方的关联被切断了接收方可能收到自己不该关心的消息也可能在页面不可见时收到消息后去操作UI导致崩溃。我的建议是能用作用域解决的尽量不用事件总线。如果一定要用至少要保证接收方在onDestroy时反注册事件。对生命周期状态做判断只处理页面可见或已恢复时的回调。避免使用粘性事件做跨页面数据传递粘性事件会在注册瞬间立即回放很容易拿到过期数据。5. 进程重建、数据恢复和通信设计的兜底策略5.1 不常被提却极易踩坑的场景进程被系统回收Activity通信不只是跳转传参还牵涉到一个被许多人忽略的大前提这个通信链路在进程被回收后还能不能成立。安卓系统为了内存回收会在后台杀掉App进程用户再切回时系统会尝试从任务栈恢复Activity。这时Activity会重建onCreate里的intent还是原来的intent但你通过事件总线、静态变量、共享ViewModel存的数据全部没了。如果你在B页面依赖A的临时数据而A已经被回收B恢复时就会出问题。要识别这个场景可以观察Activity的onSaveInstanceState是否被调用。系统在Activity可能被销毁前会回调它你可以把临时状态写进去override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(draft_content, binding.editContent.text.toString()) }恢复时在onCreate里判空if (savedInstanceState ! null) { val draft savedInstanceState.getString(draft_content) binding.editContent.setText(draft) }注意这只能解决UI临时状态不能替代业务数据的持久化。5.2 数据该放哪个层级一次通信设计的关键决策我在后期做架构设计时慢慢形成了一套判断逻辑你可以直接拿去用只在这个页面一次点击流程里有效的临时状态放Intent或Activity Result。例如从列表页点进详情页要把文章id传过去这就是典型的Intent参数。用户返回后会重新读取的数据不要只依赖内存尽量持久化到数据库或DataStore。例如详情页修改了昵称返回后列表页要显示新昵称这时列表页应该在onResume里重新拉取或读取本地存储而不是依赖一个被修改过的静态变量。几十秒内的临时状态可以用ViewModel SavedStateHandle组合SavedStateHandle可以在进程被系统回收时自动保存一小部分轻量数据适合表单、选择器状态。全局业务配置用持久化存储启动时加载到内存单例但内存单例只作为缓存不做唯一数据源。5.3 通信链路的日志可观测性最后分享一个实践心得。复杂App里Activity之间的通信链路一多出Bug很难靠肉眼定位。我在项目里的做法是给所有关键通信点打结构化日志Log.d(Navigation, MainActivity - DetailActivity, extras: {article_id1024, sourcelist}) Log.d(Navigation, DetailActivity setResult: {article_id1024, is_collectedtrue})日志格式统一成发起方 - 目标方 关键参数的样式线上排查时配合日志平台能快速看到用户操作到哪一步数据没传对。对比那种靠Toast、靠断点一步步查的方式效率完全不是一个量级。尤其是Activity Result回调链路里日志会明确告诉你结果有没有设置、resultCode是什么、data里到底有没有携带目标字段。顺着日志找问题比无头苍蝇式猜测靠谱得多。真要说起来Activity之间的通信方案没有银弹。核心原则是让数据待在该待的地方别图省事把生命周期不匹配的方案生搬硬套。每次动手前多问一句这个数据在页面重建后还能不能活着很多线上崩溃就能提前避免。