
看到这个标题估计不少做Android开发的朋友都有同感Intent传值听上去是个再基础不过的知识点。可实际开发中我见过太多人卡在“拿不到值”“类型转换崩溃”“外部Scheme唤起后参数是空的”这些坑里。尤其是这两年支付宝这类超级App的intent://platformapi/startapp唤起链路被反复讨论连带着“Intent到底怎么取值”又成了热门搜索关键词。这篇文章就把Intent传值的方方面面一次性讲透。不管你是刚入门的新手还是被线上问题逼着查源码的老兵我都会从最基础的getIntent()开始逐步深入到Bundle传递、对象序列化、外部Scheme参数解析最后把我在实际项目中踩过的坑和排查方法一并整理出来。建议先收藏再读遇到问题可以直接对照排查。1. Intent传值基础先从Activity里拿到数据1.1 getIntent()获取Intent实例为什么必须先判断nullIntent传值的核心链路很简单发送方把数据塞进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)需要强转且要处理ClassNotFoundExceptionintent.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); // 没传Intentdata为null // 正确示范 Intent resultIntent new Intent(); resultIntent.putExtra(result, success); setResult(RESULT_OK, resultIntent); finish();第三数据量太大。Intent虽然能传数据但它本质上是Binder跨进程通信的携带物数据量太大会触发TransactionTooLargeException异常。这个问题在后面章节详细展开。2. Bundle与对象传值把复杂数据搬进Intent2.1 putExtra vs getExtras()为什么建议用Bundle统一管理新手往往喜欢一路putExtra到底但当你需要传递十几个参数时这种写法会让代码变得非常凌乱。更合理的做法是用Bundle先把数据打包再统一塞进IntentBundle 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。两者的区别值得认真对待对比项ParcelableSerializable性能高专为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 CreatorUser CREATOR new CreatorUser() { 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?appid20000067url...。这种以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?appid20000067...这类链接通常由支付宝等App自行定义并处理。对普通开发者来说核心任务是两件第一给自家App配置Scheme让外部链接能唤起第二正确解析Intent中携带的Uri参数。下面两节分别讲。3.2 在AndroidManifest里注册Scheme过滤器要让自己App能被外部链接唤起需要在目标Activity的intent-filter里声明schemeactivity android:name.SchemeHostActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.VIEW / category android:nameandroid.intent.category.DEFAULT / category android:nameandroid.intent.category.BROWSABLE / data android:schememyapp android:hostproduct android:pathPrefix/detail / /intent-filter /activity几点说明android:scheme必填相当于App的“身份证号码”。比如intent://platformapi/startapp里的scheme就是intent不过这通常是通用方式很多App会选一个独特的前缀比如taobao://、weixin://。android:host和android:pathPrefix用于精确匹配限制只有特定链接才能唤起这个页面。如果不写host会导致许多无关链接也能拉起App。android:exportedtrue在Android 12及以上是必须显式声明了否则外部应用无法唤起。配置完成后别人就能通过这样的链接拉起你的页面myapp://product/detail?productId123456sourcehomepage关于热搜词里出现的intent://platformapi/startapp?appid20000125ordersuffixh5_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或singleTopApp退到后台后再次从外部链接或通知栏唤起时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 数据被截断或类型错乱场景一传超大数据量。接续前面提到的TransactionTooLargeExceptionIntent是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?productId123这样的链接自动映射到指定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从推送点进详情页详情页内部又跳转WebViewWebView又需要拿到登录态和来源标识。这一套链路中的Intent转发和参数拼接也值得提前设计否则每层都丢几个字段到了终点页面就只剩下一个空壳了。Intent看似只是简单的数据容器但它承载的是Android组件之间的通信契约。把“获取intent传过来的值”这件小事做扎实、做规范页面之间的衔接会更顺畅线上问题也会少一个类别。希望这篇文章能帮你理清头绪少走一些我当年绕过的弯路。