看到这个标题,估计不少做Android开发的朋友都有同感:Intent传值,听上去是个再基础不过的知识点。可实际开发中,我见过太多人卡在“拿不到值”“类型转换崩溃”“外部Scheme唤起后参数是空的”这些坑里。尤其是这两年,支付宝这类超级App的intent://platformapi/startapp唤起链路被反复讨论,连带着“Intent到底怎么取值”又成了热门搜索关键词。
这篇文章就把Intent传值的方方面面一次性讲透。不管你是刚入门的新手,还是被线上问题逼着查源码的老兵,我都会从最基础的getIntent()开始,逐步深入到Bundle传递、对象序列化、外部Scheme参数解析,最后把我在实际项目中踩过的坑和排查方法一并整理出来。建议先收藏再读,遇到问题可以直接对照排查。
1. Intent传值基础:先从Activity里拿到数据
1.1 getIntent()获取Intent实例:为什么必须先判断null
Intent传值的核心链路很简单:发送方把数据塞进Intent,接收方在onCreate里用getIntent()拿回这个Intent,然后通过getStringExtra、getIntExtra这类方法把数据取出来。很多新手写代码是这样的:
String userId = getIntent().getStringExtra("userId");这段代码看着没问题,但有个隐患——如果当前Activity是被系统重建的,比如内存不足被回收后恢复,这时候getIntent()返回的仍然是最初启动时的Intent,倒不会为null。真正危险的是某些极端场景下,比如你在Fragment中尝试拿宿主Activity的Intent,或者自定义View里误用了getContext().getIntent()这种不存在的方法,IDE会直接报错。
更稳妥的写法是先判空再取值:
Intent intent = getIntent(); if (intent != null) { String userId = intent.getStringExtra("userId"); if (userId != null) { // 业务处理 } }如果用Kotlin,建议这样写:
val userId: String? = intent?.getStringExtra("userId")intent?这个问号不是多余的,它保证在极端情况下不会因为空指针直接闪退。说句实在话,getIntent()返回null的概率极低,但养成判空习惯没有坏处。真正容易出问题的是getStringExtra返回null——发送方压根没塞这个key,或者塞了整个Bundle为null,你直接使用返回值就会崩。
1.2 getXxxExtra系列方法:数据类型对应关系速查
Intent传值支持的数据类型相当丰富,我把最常用的整理成表格,方便对照使用:
| 发送方调用方法 | 接收方获取方法 | 常见坑点 |
|---|---|---|
intent.putExtra("key", "字符串") | getStringExtra("key") | 拿不到返回null,不是空字符串 |
intent.putExtra("key", 123) | getIntExtra("key", 0) | 必须提供默认值,否则类型不匹配崩溃 |
intent.putExtra("key", true) | getBooleanExtra("key", false) | 同上 |
intent.putExtra("key", 3.14) | getDoubleExtra("key", 0.0) | 小心float和double混用 |
intent.putExtra("key", serializableObj) | getSerializableExtra("key") | 需要强转,且要处理ClassNotFoundException |
intent.putExtra("key", parcelableObj) | getParcelableExtra("key") | 推荐方式,性能更好 |
intent.putExtra("key", bundle) | getBundleExtra("key") | 适合批量传递 |
这里要特别提醒一个老生常谈的问题:getIntExtra、getBooleanExtra这类有默认值参数的方法,如果你传的key不存在,它不会抛异常,而是乖乖返回你给的默认值。所以别指望用“取不到值”来判断数据是否存在,要用intent.hasExtra("key")或者intent.getExtras()先判断。
if (intent.hasExtra("orderId")) { String orderId = intent.getStringExtra("orderId"); }注意,hasExtra在有key但值为null时返回true,取值时依然要判null。这一点在后续排查问题时会反复用到。
1.3 拿不到值先从这几个角度排查
很多朋友问我:“明明传了值,为什么对面死活取不到?”这种问题九成出在以下几个环节:
第一,发送方和接收方的key拼写不一致。"user_id"和"userId"是两个完全不同的key,我见过有人因为下划线命名风格不统一,排查了整整半天。
第二,发送方用的Intent和接收方收到的Intent不是同一个。比如你用startActivityForResult启动,然后在onActivityResult里通过data取返回值,但发送方压根没把数据放在回传Intent里。
// 错误示范:忘记把resultIntent包装 setResult(RESULT_OK); // 没传Intent,data为null // 正确示范 Intent resultIntent = new Intent(); resultIntent.putExtra("result", "success"); setResult(RESULT_OK, resultIntent); finish();第三,数据量太大。Intent虽然能传数据,但它本质上是Binder跨进程通信的携带物,数据量太大会触发TransactionTooLargeException异常。这个问题在后面章节详细展开。
2. Bundle与对象传值:把复杂数据搬进Intent
2.1 putExtra vs getExtras():为什么建议用Bundle统一管理
新手往往喜欢一路putExtra到底,但当你需要传递十几个参数时,这种写法会让代码变得非常凌乱。更合理的做法是用Bundle先把数据打包,再统一塞进Intent:
Bundle bundle = new Bundle(); bundle.putString("name", "张三"); bundle.putInt("age", 28); bundle.putString("phone", "13800138000"); Intent intent = new Intent(this, DetailActivity.class); intent.putExtras(bundle); startActivity(intent);接收方取数据时,既可以直接用intent.getExtras()拿到整个Bundle,再从此取值:
Bundle bundle = getIntent().getExtras(); if (bundle != null) { String name = bundle.getString("name"); int age = bundle.getInt("age", 0); }用Bundle的核心价值在于统一管理和易读性。尤其是当一个页面需要支持从多个入口进入时(比如通知栏、Scheme唤起、列表页),可以封装一个统一的createIntent(Context, Bundle)工厂方法,把所有参数入口收拢到一处。这个习惯在项目膨胀后特别有用。
2.2 Parcelable和Serializable怎么选
传递自定义对象时,可以让类实现Parcelable或Serializable。两者的区别值得认真对待:
| 对比项 | Parcelable | Serializable |
|---|---|---|
| 性能 | 高,专为Android设计 | 较低,依赖Java反射机制 |
| 编码量 | 需要手动写模板方法 | 只需声明接口,再加serialVersionUID |
| 兼容性 | 仅Android可用 | Java标准库支持 |
| 调试便利性 | 生成代码较多但清晰 | 序列化过程黑盒 |
我个人强烈推荐用Parcelable。虽然写模板代码有点烦,但性能差距在传递复杂对象、列表数据时非常明显。Android Studio有插件可以自动生成Parcelable实现代码,别再手工硬写了。示例:
public class User implements Parcelable { private String name; private int age; protected User(Parcel in) { name = in.readString(); age = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeInt(age); } public static final Creator<User> CREATOR = new Creator<User>() { @Override public User createFromParcel(Parcel in) { return new User(in); } @Override public User[] newArray(int size) { return new User[size]; } }; }发送方与接收方代码:
// 发送方 intent.putExtra("user", user); // 接收方 User user = getIntent().getParcelableExtra("user");有一点必须注意:反序列化后的对象是全新的对象,不是原来那个对象实例。如果你对接收方的对象做了修改,不会再同步回发送方。这跟Java的引用传递认知不同,Intent跨组件传递本质上是在做一次序列化和反序列化。
2.3 自定义对象传值的完整写法
我把一个完整的使用场景写出来,大家可以直接套用。假设需求是:从订单列表页跳转到订单详情页,需要传递订单对象和一个来源标记。
// 订单对象,实现Parcelable public class Order implements Parcelable { private String orderId; private double amount; private int status; // 省略构造、getter、setter、Parcelable模板代码 } // 列表页跳转 public static Intent createOrderDetailIntent(Context context, Order order, String source) { Intent intent = new Intent(context, OrderDetailActivity.class); Bundle bundle = new Bundle(); bundle.putParcelable("order", order); bundle.putString("source", source); intent.putExtras(bundle); return intent; } // 详情页接收 Bundle extras = getIntent().getExtras(); if (extras != null) { Order order = extras.getParcelable("order"); String source = extras.getString("source"); // 根据source做不同的埋点统计 }这种写法把Intent构造和解析内聚在业务类旁边,方便维护。如果参数多了、入口多了,建议再用一个OrderDetailParams之类的数据类统一承载,避免散落各处。
3. 外部Scheme唤起:intent://背后的取参逻辑
3.1 什么是intent://platformapi/startapp
很多人在搜索栏敲过“intent支付宝”,其实就是指支付宝对外提供的URL Scheme唤起链接,形如intent://platformapi/startapp?appid=20000067&url=...。这种以intent://开始的字符串,专业叫法是intent scheme或intent URL,作用是从浏览器、短信、另一个App直接拉起特定App并携带参数。
深入了解之前,要先区分两大类Intent:显式Intent(指定包名和类名的)和隐式Intent(通过action、data、category来让系统匹配)。外部Scheme链接是隐式Intent的一种落地方式。当用户在浏览器里点击intent://platformapi/startapp?xxx时,系统会解析这个链接,找到声明了对应scheme的App,并把链接中的数据包装成Intent分发给目标App。
注意,intent://platformapi/startapp?appid=20000067...这类链接通常由支付宝等App自行定义并处理。对普通开发者来说,核心任务是两件:第一,给自家App配置Scheme,让外部链接能唤起;第二,正确解析Intent中携带的Uri参数。下面两节分别讲。
3.2 在AndroidManifest里注册Scheme过滤器
要让自己App能被外部链接唤起,需要在目标Activity的intent-filter里声明scheme:
<activity android:name=".SchemeHostActivity" android:exported="true"> <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" android:pathPrefix="/detail" /> </intent-filter> </activity>几点说明:
android:scheme必填,相当于App的“身份证号码”。比如intent://platformapi/startapp里的scheme就是intent,不过这通常是通用方式,很多App会选一个独特的前缀,比如taobao://、weixin://。android:host和android:pathPrefix用于精确匹配,限制只有特定链接才能唤起这个页面。如果不写host,会导致许多无关链接也能拉起App。android:exported="true"在Android 12及以上是必须显式声明了,否则外部应用无法唤起。
配置完成后,别人就能通过这样的链接拉起你的页面:
myapp://product/detail?productId=123456&source=homepage关于热搜词里出现的intent://platformapi/startapp?appid=20000125&ordersuffix=h5_route_token%...,这类链接通常是App内H5页面跳转协议的一部分。作为接收方,你不需要关心发送方内部逻辑,只需要正确解析链接参数即可。
3.3 从Intent中解析Uri参数并做异常兜底
外部Scheme唤起App后,数据会以Uri的形式挂在Intent上。接收方这样取:
Intent intent = getIntent(); if (intent == null) return; Uri data = intent.getData(); if (data == null) return; String productId = data.getQueryParameter("productId"); String source = data.getQueryParameter("source");这里有个容易踩的坑:Uri中传过来的参数一律是String类型,需要数字就手动转换:
int productId = 0; try { productId = Integer.parseInt(data.getQueryParameter("productId")); } catch (NumberFormatException e) { // 处理格式错误,比如跳回首页 }有一点非常关键,但很多文章不提:你不是只从外部链接取参数就够了。优先取Intent里的参数,再回退到Uri里的参数,这是常见的兼容策略。因为有些渠道(比如某些推送SDK)会把参数塞进Intent的extra里,而浏览器Scheme唤起时参数在data里。
String source = intent.getStringExtra("source"); if (source == null && data != null) { source = data.getQueryParameter("source"); }再加上URL编码问题。链接里的中文、特殊符号通常会做URL编码(%3a、%2f等),getQueryParameter方法会自动解码,一般不需要你手动URLDecoder.decode。但如果你直接用data.getQuery()去手动解析,就必须自己处理编码问题,否则拿到的是一堆乱码或%开头的字符串。
4. 实战中的坑与排查技巧
4.1 onNewIntent拿不到值怎么办
这是后台开发者问得最多的问题。情境是这样的:Activity的启动模式设置为singleTask或singleTop,App退到后台后,再次从外部链接(或通知栏)唤起时,onCreate不会重新执行,而是走onNewIntent。但你在onCreate里写的取参逻辑没有执行,所以页面显示的还是旧数据。
解决方案是重写onNewIntent,并用新Intent替换旧的Intent引用:
@Override protected void onNewIntent(Intent newIntent) { super.onNewIntent(newIntent); setIntent(newIntent); // 关键步骤 handleIntent(newIntent); }注意,必须在onNewIntent里先调用setIntent,再调用处理逻辑。否则你在处理方法内部如果用getIntent()取值,拿到的还是旧的Intent,处理完等于没处理。这个顺序我见过不止三个人写反。
handleIntent是抽出来的公共方法,在onCreate和onNewIntent里都调用:
private void handleIntent(Intent intent) { Uri data = intent.getData(); if (data != null) { refreshPage(data.getQueryParameter("productId")); } }如果是singleTask模式,当Task里已经有该Activity实例时,新Intent会直接传给onNewIntent;如果Task里没有该Activity实例,则会先走onCreate。所以两个方法都必须处理好取参逻辑,不能只处理一个。
4.2 数据被截断或类型错乱
场景一:传超大数据量。接续前面提到的TransactionTooLargeException,Intent是Binder通信的载体,Binder的传输缓存是有上限的(大约1MB左右)。你硬塞一个几MB的Bitmap进去,轻则异常,重则直接Crash。
解决思路:对象传引用,大文件传路径或ID。比如图片,把图片路径或Uri传给目标页面,目标页面再自行加载图片。比如业务数据,只传一个主键ID,目标页面通过数据库或网络接口再次查询详情。这是一条架构层面的铁律:Intent适合传轻量级参数,不适合当数据仓库。
场景二:类型错乱。比如发送方用了putExtra("data", someParcelable),接收方却用getStringExtra("data")。这种情况通常会抛出ClassCastException(在取数时才会触发)。避免口诀是:发送接收两边代码保持同步更新。如果项目是多模块多人协作,最好把Intent key和参数类型定义在一个公共类里,避免各写各的。
场景三:自定义Parcelable类混淆问题。如果你开启代码混淆,而Parcelable类没有被配置混淆规则,反序列化时会出现“BadParcelableException”或者字段值全部为null。解决方案是在ProGuard/R8规则文件中添加:
-keep class com.yourpackage.bean.** { *; }4.3 我平时调试Intent的几种方式
有时候你排查半天也不知道参数到底传没传过来。最简单粗暴的方式,是直接把Intent完整打印出来:
Log.d("IntentDebug", getIntent().toString());Intent.toString()会把action、data、type、flags、extras(包括所有key-value)全部打出来,效果非常直观。如果你用了Parcelable自定义对象,extras里会显示对象类型,但不会打印对象内部字段。要查看具体字段,你可以在Bean里自己实现toString()然后在调试时手动打印:
User user = getIntent().getParcelableExtra("user"); Log.d("IntentDebug", user.toString());另外,Android Studio自带的Android Debug Database插件、以及直接在断点模式下展开intent.mExtras的树形结构,都是排查Intent问题的利器。我通常会在目标页面的onCreate和onNewIntent第一行打日志,看看哪个入口被触发了、参数是什么。这套组合基本能解决九成的Intent取值问题。
还有一点经验:外部Scheme唤起后,如果拿不到参数,先检查intent-filter里的android:scheme、android:host、android:pathPrefix是否配置得太严,尤其是pathPrefix写错会导致链接命中不了页面,根本轮不到取参这一步。再检查发送方链接拼写,很多链接参数里的&在XML里被转义或者被某些渠道拦截,导致Uri不完整。
5. 取参的扩展方案与现代化替代
5.1 深链接框架:让Scheme解析不再裸写
上一节讲的裸解析Uri参数方式,在页面少、参数简单的项目里完全够用。但当一个App有十几个页面需要支持外部唤起时,每页都写一遍getQueryParameter就很痛苦了。这时可以考虑引入深链接框架。
目前市面上主流方案有比较好用的路由框架,比如阿里系的ARouter、美团的WMRouter,以及基于AndroidX Navigation的隐式深链接。它们做的事情本质上是一样的——将myapp://product/detail?productId=123这样的链接自动映射到指定Activity,并把参数注入进去。
以ARouter为例,你只需要在Activity上写一行注解:
@Route(path = "/product/detail") public class ProductDetailActivity extends AppCompatActivity { @Autowired String productId; @Autowired(name = "source") String source; }然后在入口处统一处理:
ARouter.getInstance() .build("/product/detail") .withString("productId", "123456") .withString("source", "homepage") .navigation();外部链接通过ARouter的getInstance().build(uri).navigation()方式接进来。参数自动填到标注了@Autowired的字段上,省去手动取值的步骤。这类框架对Intent传值的价值不在于替代,而在于把“获取参数”从散落的模板代码收敛成统一机制。
不过引入框架也需要权衡。如果只是两三个页面、几个参数,老老实实用原生Intent反而更加可控、无额外依赖、出现问题好排查。我给团队同学的建议是:项目刚起步先用原生,等页面数和入口数明显变多,再考虑框架化。
5.2 带着Context和返回值:用startActivityForResult时注意的取值细节
除了向目标页面传值,有时候你还需要目标页面返回数据给前一个页面。这时候用startActivityForResult启动,在目标页面通过setResult设置返回值。但有一个很多新手搞混的点:目标页面返回之前,要用Intent携带数据,并且前一个页面要在onActivityResult中取,而不是在onCreate里重复取。
// 前一个页面 Intent intent = new Intent(this, SelectCityActivity.class); startActivityForResult(intent, REQ_CODE); protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQ_CODE && resultCode == RESULT_OK) { if (data != null) { String city = data.getStringExtra("selectedCity"); } } }同样的,返回数据不能靠getIntent()跨页面拿,那是拿不到新数据的。必须通过data参数来读取。
5.3 尽量少传大对象:接口传ID,页面自己查数据
我前面提到过大对象会导致Binder缓存溢出,这里再展开说一条设计心法。与其传整个Order对象,不如只传orderId,目标页面通过数据仓库去查详情。这样做的好处很多:
- 避免了Parcelable序列化和反序列化的性能开销;
- 避免了对象数据过期的问题——发送方持有的Order可能已经更新了,你收到的却是旧快照;
- 避免了混淆、类型错误、字段丢失等一连串问题。
当然,如果目标页面必须立即展示大量数据,不能等待网络请求,那适当传一些轻量级字段(id、标题、来源)也是合理的。关键在于“够用为止”,不要图省事把整个大对象连同内部列表全塞进去。
6. 最后的实用建议
Intent取值这件事,越简单越不容易出错。刻意追求花哨的传递方式,往往会给维护埋下隐患。就我个人的开发体会来说,初期养成三个好习惯能少踩很多坑:
第一,取Intent参数的代码统一放在Activity的开头位置,不要散落在业务方法里。这样出问题了一眼就能看到“它是从哪里拿的数据”。
第二,所有Intent的key用常量管理,不要边写边硬编码字符串。"userId"直接在代码里出现三次以上,就该抽成companion object或常量类了。到时候重构起来你会感谢自己。
第三,遇到从外部链接唤起后参数不对的情况,先打日志看Intent的toString()内容,再检查Manifest配置,最后再怀疑代码逻辑。多调试几回,排查速度会快很多。
另外,如果你接入了第三方SDK,注意它们拉起的页面也要做Uri参数透传。很多App从推送点进详情页,详情页内部又跳转WebView,WebView又需要拿到登录态和来源标识。这一套链路中的Intent转发和参数拼接也值得提前设计,否则每层都丢几个字段,到了终点页面就只剩下一个空壳了。
Intent看似只是简单的数据容器,但它承载的是Android组件之间的通信契约。把“获取intent传过来的值”这件小事做扎实、做规范,页面之间的衔接会更顺畅,线上问题也会少一个类别。希望这篇文章能帮你理清头绪,少走一些我当年绕过的弯路。