避坑指南:正确获取Intent传递的值,这几种方式我全给你捋明白了
做Android开发,谁还没跟Intent打过交道?启动Activity、传参数、接收返回值、处理外部链接调起,几乎每个页面跳转背后都有Intent在默默干活。我早期写项目的时候,在“获取Intent传过来的值”这件事上踩过不少坑——不是取出来是null,就是类型转崩了,还有一次是从一个第三方App的链接协议里死活拿不到参数,后来发现是自己解析的方式压根就不对。
这篇文章我就把Intent传值的完整链路和实操要点都摊开讲清楚。从最基础的getExtra,到带类型的getStringExtra、getSerializableExtra,再到处理Uri和scheme协议的深度链接场景,最后附上一份常见问题排查表。不管你是刚接触Android的新手,还是已经写过几个项目的老手,这篇文章都能让你少走一些弯路。
1. 先从根上理解Intent传值的本质
1.1 Intent到底是什么,它在传值链路里扮演什么角色
很多教程会告诉你“Intent是Android用于页面间通信的桥梁”,这话没错,但不够具体。本质上Intent就是一个传递意图的数据载体,它封装了三样东西:你想让系统做什么(动作),你想让系统在哪个目标上做(组件或数据),以及你做这件事需要携带的附属信息(额外参数)。
传值这件事,用的就是最后那部分——Extra数据。你可以把Intent理解成一个快递包裹,action和data是包裹上填写的收件地址和配送方式,而Extra数据就是包裹里塞的实物。收件人(目标Activity)拿到包裹后,需要拆开它、取出里面的实物来用,这个“拆包裹”的动作,就是我们说的“获取Intent传过来的值”。
与Web开发做个对比可能更好理解:Intent类似URL里的query参数,Intent.putExtra()就是在拼URL参数,而getIntent().getStringExtra()就是服务端从query里取值。但Android比URL复杂的地方在于,Extra支持的数据类型非常多,从基础类型到Bundle再到Parcelable对象,因此取值的方式也就分成了好几种。
1.2 源码层面看一下:Extra数据到底存哪里了
实际使用的时候我们很少会去想:putExtra()传进去的值,底层是怎么存的?直到有一天我在追一个大型项目的OOM问题时发现,某个页面把图片的base64字符串塞进了Intent的Extra,而那个Intent因为某些原因被保存了下来,整个进程直接内存飙升。从那以后我才认真去翻了源码。
Intent内部维护着一个Bundle mExtras字段。你每次调用putExtra(String key, value),它会检查mExtras是否为null,如果是就new一个Bundle,然后把key-value塞进这个Bundle里。所以,Intent传值的本质就是向一个Bundle写入数据,而获取值的本质就是从这个Bundle按key读取数据。
有意思的是,Android的Activity启动流程中,startActivity()传入的Intent会经历一次Binder跨进程传输。如果只是同App内页面跳转,走的是Binder直接传输,效率还行;但如果涉及跨App的Intent(比如系统相册、第三方支付SDK回跳),数据会被序列化后再反序列化。这时候就得注意:不是所有类型都可以放进Intent里跨进程传,普通对象必须实现Serializable或Parcelable,否则直接崩。
2. 获取Intent传值的五种姿势,逐个拆解
“获取Intent传过来的值”这句话看着简单,实际展开会发现有好几种情况:有的值是从A页面跳B页面时带的,有的是上层Fragment里要接,有的是从服务、广播接收器里来,还有的是外部链接调起App时附带的参数。不同场景,取值的入口不完全一样。
2.1 最标准的姿势:getIntent().getXxxExtra()
这是绝大多数人日常用的方式。目标Activity里通过getIntent()拿到启动自己的那个Intent对象,然后根据之前putExtra()时的key来取。
// 发送方 Activity A Intent intent = new Intent(this, BActivity.class); intent.putExtra("user_id", 10086); intent.putExtra("user_name", "张三"); intent.putExtra("user_info", new UserInfo("张三", 18)); // 需要实现Serializable startActivity(intent);// 接收方 Activity B Intent intent = getIntent(); if (intent != null) { int userId = intent.getIntExtra("user_id", -1); String userName = intent.getStringExtra("user_name"); UserInfo userInfo = (UserInfo) intent.getSerializableExtra("user_info"); }这里有三个细节容易被忽视,我单独拎出来说:
第一,getStringExtra()如果key不存在或者值是null,会返回null。所以取值后如果要用它做判断或拼字符串,最好先判空。
第二,基础类型要提供默认值。拿getIntExtra("user_id", -1)为例,第二个参数就是key不存在时的兜底值,用它来避免魔法数字问题是个好习惯,比拿到0再猜含义要清晰得多。
第三,Object类型强转的时候务必做类型匹配检查。Service里经常能看到类似String data = (String) intent.getSerializableExtra("data")的写法,一旦存进去的类型跟取出来的不一致,运行期直接ClassCastException,连提示都很隐蔽。
2.2 用IntentFilter接收外部调起的场景
如果App配置了Scheme或intent-filter,那么外部链接(比如H5页面、短信内容、扫码结果)可以直接调起你的Activity。这时候的Intent来自系统解析后的结果,取值的方式会有些不同:
// AndroidManifest.xml 中配置 <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="myapp" android:host="product" /> </intent-filter>// 接收解析 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val intent = intent val data: Uri? = intent?.data if (data != null) { val productId = data.getQueryParameter("productId") val from = data.getQueryParameter("from") } }这种场景下,传值的行为逻辑已经从putExtra()变成了往URL后面拼参数。用getQueryParameter()来取比直接手写字符串截取靠谱得多,因为Uri已经帮你把url解码和参数解析都做完了,不容易出现编码问题或数组越界。
2.3 Fragment之间用Arguments传递的场景
用Fragment的时候,很多人在构造函数里直接传参,这是我非常不建议的做法。Android系统在Activity重建时会自动恢复Fragment,如果你在构造里传了参数而系统重建时用的是无参构造,参数就丢了。官方推荐的做法是通过arguments来传:
class DetailFragment : Fragment() { companion object { private const val ARG_ID = "arg_id" fun newInstance(id: String) = DetailFragment().apply { arguments = Bundle().apply { putString(ARG_ID, id) } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val id = arguments?.getString(ARG_ID) } }为什么要把参数放在arguments里而不是直接放成员变量?因为Fragment的arguments是专门设计用来保存初始参数的Bundle,它会在Fragment重建时自动保留。Activity因为进程被系统回收而重建时,FragmentManager会通过arguments恢复Fragment实例,你的参数就不会莫名其妙丢失。这是很多项目上线后才发现的问题——用户切后台再打开,页面数据全空了,往往就是这个原因。
2.4 Service和BroadcastReceiver的Intent取值
页面跳转之外,Service和BroadcastReceiver也会收到Intent,只是入口不同。比如你用startService()传参数,Service里接收的Intent就是你在onStartCommand()里拿到的那个;而你发广播时塞进去的Extra,则要在onReceive()里从intent参数中取出。
// 发送 Intent serviceIntent = new Intent(this, UploadService.class); serviceIntent.putExtra("file_path", filePath); startService(serviceIntent); // 接收,Service的onStartCommand中 @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String filePath = intent.getStringExtra("file_path"); } return START_STICKY; }一个容易踩的坑是:onStartCommand()的intent参数在多次startService()调用时可能为null。因为Service如果已经被系统以START_STICKY模式重启了,重启时系统给你的可能是一个空Intent。所以在Service里随手判空是个好习惯,直接取肯定会NPE。
2.5 高阶用法:startActivityForResult的返回值获取
如果说前面几种都是单向传值,那startActivityForResult()就是双向通信了。A页面启动B页面,B页面把处理结果通过setResult()回传,A页面在onActivityResult()中接收。
// 接收返回值 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_PICK_IMAGE && resultCode == RESULT_OK) { if (data != null) { Uri selectedImage = data.getData(); String path = data.getStringExtra("path"); } } }注意resultCode和data都有可能为null。setResult()后用户按返回键,系统给的结果码可能是RESULT_CANCELED,同时data为null;而有些系统相机App在用户取消后连resultCode都不回,所以onActivityResult里三件套——requestCode校验、resultCode校验、data判空,一个都不能省。
3. 深度剖析:Intent取不到值、返回null的几种常见原因
在实际开发中,新手问的最多的就是:“我明明put了值,怎么get出来是null?”这个问题背后的原因其实不那么直观,我总结了几个最常见的,一条条对号入座。
3.1 key名字不一致
这是最简单也最容易被忽视的。发送方用的是"user_name"作为key,接收方却写了"userName"。因为putStringExtra和getStringExtra在编译期不会校验key的存在性,程序照常运行,但取值结果就是null。很多项目里定义一个常量类专门管理Intent key,就是干这个用的:
public class IntentKeys { public static final String USER_NAME = "user_name"; public static final String USER_ID = "user_id"; }这样两边都引用IntentKeys.USER_NAME,至少能杜绝手写字符串不一致的尴尬。
3.2 类型不匹配导致的ClassCastException
这是一个比较隐蔽的问题。你在A页面putSerializableExtra("info", object)塞进去的某个对象,取的时候如果用getParcelableExtra()去取并且强转,运行期就崩了。尤其是项目里有人习惯用putExtra(String, Object)这种重载,让Java在编译期无法帮你做类型检查,问题就藏得更深。务实做法是:固定每个key的数据类型,取的时候也只用对应的方法,做到“什么类型进、什么类型出”。
3.3 进程被杀后的Intent恢复问题
当App退到后台时间长了,系统可能回收Activity实例,你从桌面点回来,系统会用保存的状态重建页面。如果当初启动Activity的Intent里放了大量数据(比如整个列表、图片大对象),这些数据会被系统缓存起来,恢复时也会带着。坑点在于:如果这些数据的类有改动(增删了字段、改了包名),反序列化时就会抛异常或返回null。所以我的习惯是,Intent里尽量只传轻量的标识参数,真正的大对象让接收方通过id自己再查一次,这是最稳的架构。
3.4 在onNewIntent里没处理
如果你的Activity是singleTask或singleTop启动模式,那么当它已经存在于栈顶时再次被startActivity()启动,系统不会走onCreate,而是走onNewIntent()。如果你只在onCreate里用了getIntent(),第二次调起传的新值就永远拿不到。
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); // 重新设置Intent,后续再getIntent()才是新值 }处理方案很简单:在onNewIntent里调用一次setIntent(intent),把新intent设为当前intent,再走一遍数据解析逻辑。这条我看到太多项目漏掉了,属于低频但高影响的问题。
3.5 混淆规则引发的问题
如果你做了代码混淆,而又在Intent里传递了自定义对象,那么反序列化时如果混淆规则没有保留类的成员名和序列化UID,就会导致字段匹配不上,取出来全是null,甚至直接崩。
-keep class com.yourpackage.model.** { *; } -keepclasseswithmembernames class * { native <methods>; } -keepclassmembers class * implements java.io.Serializable { private static final java.io.Serializable serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; }做电商类App的同学尤其要注意,商品、订单、用户这类对象几乎都会放在Intent里传递,加上混淆后出一次线上事故的排查成本,足够让你养成良好习惯:要么保留这些model的混淆白名单,要么干脆只传id,接收方再用网络或本地存储拿完整数据。
4. Intent解析外部链接:从scheme到参数,一步到位
前面提到过,Android的Intent-filter可以接收外部链接,这也是标题关联热词里“intent://platformapi/startapp”这种协议能跑通的核心机制。理解了这个,你再去看各种三方App之间的互调链接就不慌了。
4.1 intent://协议的本质
intent://这种链接格式是Android系统提供的一种特殊的URI,它的行为是尝试通过Uri的scheme来匹配已注册的IntentFilter。你可以理解为这是一种“用URL触发Android原生Intent”的方式——浏览器或任意应用里点一个intent://链接,系统会解析出里面的scheme、host、path以及附加参数,然后去找能响应的Activity并拉起它。
第三方平台(比如支付类App、地图类App)的调起协议很多都基于这种机制,格式一般是:
intent://platformapi/startapp?appid=20000067¶m=xxx#Intent;scheme=alipays;action=android.intent.action.VIEW;end拆解来看,intent://之后跟着的是具体业务路径和参数,#Intent之后则是跟随的Intent配置信息——比如scheme、action、category、package等。系统解析时会把#Intent后面的配置作为Intent的骨架,把query参数作为数据附加进去。
4.2 应用内如何解析这类协议
如果你在自己的App里也要接收类似的协议链接,常规做法是三步:
第一步,在Manifest里声明自己能接收哪些scheme的调起,就像前面代码里写的那样。
第二步,在目标Activity中获取intent.getData(),得到完整的Uri。
第三步,调用Uri的API把业务参数拆出来。
val uri: Uri? = intent.data val appId = uri?.getQueryParameter("appid") val bizData = uri?.getQueryParameter("url")有一种容易被忽略的情况是,#Intent里有时还会带S;开头的附加参数,比如S.caller=xxx,这种参数不会落在query里,而是直接放在Intent的extras中。你要用intent.getStringExtra("caller")这种形式来取,而不是从Uri里拿。我的经验是:拿到链接后先别急着写解析逻辑,把它完整打出来看一遍,分清哪些在query、哪些在extras,再动手。
4.3 处理外部调起时别忽略前后台状态
外部链接调起你的App时,目标Activity可能已经存在,也可能需要新建。如果Activity用的是launchMode="singleTask"或singleTop,那你依然得靠onNewIntent()来接数据。很多从H5或短信跳转过来的场景,点了链接后App没有重新走onCreate,数据解析又写在onCreate里,结果就表现为“参数丢了”。
再说一个和它紧密相关的问题:如果App进程被系统回收了,点击外部链接拉起的是一个全新进程,所有的初始化都要重走。这个时候如果目标Activity的启动模式是singleTask,系统会先重建一个任务栈,再恢复你之前记录的任务。整个流程复杂,所以我的做法是,把链接的参数解析统一收敛到一个工具类里,无论是onCreate还是onNewIntent最终都调用同一个解析入口,避免两套逻辑都对不上。
5. 序列化选择:Serializable还是Parcelable
传对象是Intent传值里绕不开的需求。两个人开发同一个项目,很可能一个人习惯用Serializable,另一个人习惯用Parcelable,时间久了代码里两种风格混着来。这两种方式各有优劣,选错了在性能或代码维护上都会有点难受。
5.1 两者的核心差异
Serializable是Java原生的序列化机制,代码侵入简单——你的Bean实现个接口就行,剩下的交给JVM反射处理。但反射的效率天然比Parcelable这种手写协议低,而且在混淆以后问题很多,这在前面的章节也提到过。
Parcelable是Android官方推荐的方式,它不依赖反射,而是把对象拆解成一个个基本字段,写入Parcel这个“快递单”里。性能上比Serializable高不少,但需要你手写writeToParcel、createFromParcel等一堆样板代码,字段一多,维护起来就头大。好在现在有不少插件和注解库能自动生成,手写的痛苦基本可以缓解。
5.2 我的选择建议
如果只是单个简单对象,比如一个用户信息、一个订单摘要,字段不超过十个,用什么其实性能差别不大。但如果是在列表页和详情页之间频繁传对象、或者在多进程场景下,Parcelable更靠谱。还有一种情况是你会把这个对象存进本地持久化文件或者通过推送SDK传递,那Serializable反而更通用一些。
不管是哪种,都建议把对象设计得“瘦一点”。别图省事把整个列表对象塞进Intent,一次传几十个数据对象的后果是页面跳转肉眼可见地卡顿,因为序列化和反序列化都要花费时间。传个id数组,接收方再查详情,这是高并发、大列表场景下的常见取舍。
6. 常见问题速查表:按症状对号入座
我把这些年开发过程中遇到的“Intent取值相关”的经典问题整理成一张速查表。项目排查时直接按表翻,比重新看每个页面逻辑快得多。
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
getStringExtra()返回null | key不一致或发送方没put | 检查key拼写,统一用常量类 |
| 取出来的对象强转报类型异常 | put和get用了不同的方法 | 固定数据类型的put/get配对使用 |
| 第二次调起页面拿到的还是旧值 | 启动模式导致走了onNewIntent | onNewIntent里调用setIntent,重新解析 |
| 混淆后取出来的对象字段为null | 混淆规则未适配Serializable | 配置proguard规则,保留model成员名 |
| 从H5跳过来参数全拿不到 | scheme匹配到了别的Activity或解析位置不对 | 确认IntentFilter配置和解析入口 |
| 大对象放进Intent后跳转很卡 | 序列化耗时过长 | 改为传id,接收方再查数据 |
| 进程被杀后恢复页面时数据丢了 | Intent保存的序列化对象与当前类不兼容 | 传轻量参数,重建时重新拉取数据 |
| onActivityResult中data为null | 用户取消或目标页面没setResult | 判空并校验resultCode |
这张表不完整,但覆盖了我在实际项目里遇到概率最高的几个问题。如果你遇到的其他情况不在表里,欢迎自己再做一轮排查,方法论其实都是相通的:先看key和值类型对不对,再看Activity生命周期和启动模式,最后才去考虑序列化和混淆这些深层次问题。
7. 一些长期有效的习惯
代码写得多了,我发现Intent传值这块的很多问题不是技术难度大,而是习惯不好。趁着最后再分享一些我用了很多年的做法。
第一,创建IntentKeys常量类,把App所有Intent相关的key统一管理起来。项目小的时候可能觉得多余,但项目一旦超过二十个页面,散落的字符串key迟早会坑你一次。
第二,接收数据的代码局部化。在Activity里单独写一个parseIntentData()方法,不要跑到业务逻辑中间去顺手getExtra。这样无论是调试排查还是后续维护,定位都会很快。
第三,能用startActivityForResult/Activity Result API的时候,不要用静态变量传值。静态变量传值确实省事,但如果进程被杀,静态变量会重置,回传的数据当场消失。
第四,写工具方法统一处理Uri参数解析。外部链接的解析逻辑尽量写在一个类里,不要每个Activity复制一份,因为Uri的解析规则稍微变动,你要改的就不止一个地方。
第五,尽量传标识,不要传整个对象。这条原则在大项目里尤其重要,它不仅能减少序列化开销,还能避免因为数据类型变化导致的历史兼容问题。如果你能做到九九成的情况下Intent里只传id,那你的代码要比大多数项目稳一个档次。
我在实际项目中见过太多因为在Intent里传超大对象结果页面掉帧、因为对象没实现序列化直接崩溃、因为混淆规则没写好导致用户信息错乱而背锅的案例。这些问题的根源几乎都不是“不会取Intent的值”,而是没有在写代码之前把传值方案想清楚。多花两分钟规划一下,能给你省下整周的排查时间。