让我们来剖析这个"痴情旧梦"的Android覆盖安装之谜,用一段故事结合源码原理讲清楚。
故事篇:时光邮差与不变的情书
想象一个叫安卓镇的地方,镇上有个痴情的书生叫小程 。他深爱着镇上的姑娘小A。
-
初遇与情书 (App A 1.0 安装):
- 小程第一次见到小A (安装 App A 1.0)。
- 他为小A写了一封炽热的情书,请求她做他的女朋友(这封情书就是
PendingIntent
)。情书里详细写明了小A家的地址(Intent
目标组件:登录页LoginActivity_v1
)。 - 小程把这封情书交给了镇上的老邮差老王 (系统服务
ActivityManagerService
- AMS)。老王有个习惯:他不会记住小A这个人本身,而是牢牢记住这封情书的内容和小程委托他时的场景(老王代表 PendingIntent 的底层机制)。 - 老王把这封情书小心翼翼地锁在了自己专属的铜盒子里(
IntentSender
),盒子上贴着标签:"小程给地址X的情书"。老王只认盒子里的情书内容和这个标签。
-
媒人牵线 (App B 触发):
- 镇上的热心媒婆小B(App B)想撮合小程和小A。
- 小B知道老王那里有小程给小A的情书盒子。她找到老王说:"老王,请把小程给小A的那封情书盒子给我,我要亲自交给小A!"(App B 调用
PendingIntent.send()
,本质是获取IntentSender
并请求 AMS 执行里面的Intent
)。 - 关键点来了:老王只认盒子(
IntentSender
)! 他拿出那个铜盒子,看着里面封存的情书内容:"送给地址X的小A"。他完全按照情书里写的原始地址 ,把这封情书送到了地址X。
-
小A的改变 (App A 2.0 覆盖安装):
- 时间流逝,小A成长了(App A 升级到 2.0)。她搬了新家(代码重构),换了新衣服(UI改版),甚至改了名字(
LoginActivity_v1
可能变成了LoginActivity_v2
,或者逻辑大改)。她的新家在地址Y。 - 但是!老王盒子里的那封旧情书,地址写的还是旧的地址X! 老王对此毫不知情 ,也不关心小A现在具体是谁、住哪里。他只认盒子里的原始指令。
- 时间流逝,小A成长了(App A 升级到 2.0)。她搬了新家(代码重构),换了新衣服(UI改版),甚至改了名字(
-
媒人再牵线与旧梦重现 (登录页旧版):
- 媒婆小B(App B)又一次想帮忙,她再次找到老王 ,请求把"小程给小A的那封情书盒子"给她(再次调用 同一个
PendingIntent.send()
)。 - 老王依然只认那个铜盒子(
IntentSender
)。他原封不动地 再次拿出旧情书,看到地址"X",于是又一次把情书送到了地址X。 - 住在地址X的是谁?是小A的1.0版本(旧版 LoginActivity)! 她虽然已经"升级"了,但安卓镇有个神奇的现象:旧版本的她(旧的Activity组件)并没有完全消失,只要还有人记得她的地址(Intent 匹配),她就能被唤醒(系统可能保留旧APK的部分信息直到进程重启)。于是,小程看到的,还是那个他最初爱上的、旧模样的小A(App A 1.0 的登录页)。
- 媒婆小B(App B)又一次想帮忙,她再次找到老王 ,请求把"小程给小A的那封情书盒子"给她(再次调用 同一个
-
新颜相见 (设置页新版):
- 媒婆小B(App B)这次想带小程去看看小A的新家(启动设置页)。
- 她没有再用老王保管的旧情书盒子。而是直接写了一张新的"寻人启事":"寻找小A,特征是'会管理设置'"(隐式
Intent
,包含 Action 如ACTION_APPLICATION_SETTINGS
或自定义 Action,可能还有 Package 名com.example.appA
)。 - 她把这张寻人启事交给了镇上的信息公告栏管理员(AMS 中的 Intent 解析机制)。
- 管理员一看:"特征'会管理设置', 属于包
com.example.appA
"。他立刻去查找当前最新注册了这个特征的人 (查找当前安装的 App A 2.0 中声明了匹配该 Intent Filter 的SettingsActivity
)。 - 管理员找到了焕然一新的小A 2.0 (新版
SettingsActivity
),并把她引荐给了小程。于是小程看到了小A崭新的一面(App A 2.0 的设置页)。
技术原理深度解析 (源码视角):
现在,我们抛开故事,用技术语言解释"老王" (PendingIntent
) 和"管理员" (隐式 Intent 解析) 的行为差异:
-
PendingIntent 的本质 - "时光胶囊"与"执行令牌":
-
当你调用
PendingIntent.getActivity()
,getBroadcast()
,getService()
时,你是在请求系统(AMS)创建一个令牌 (IntentSender
)。 -
创建这个令牌的关键参数是底层的
Intent
对象 和你的身份(调用者的 UID/PID)。系统(AMS)会将当时的Intent
对象(包含目标组件信息) 深拷贝 并存储起来。 -
核心机制:
PendingIntent
不存储目标组件的 引用 ,它存储的是创建它时那个特定Intent
对象的 快照。这个快照就像老王盒子里的那封旧情书,地址是固定死的。 -
关键源码 (
ActivityManagerService
相关 - 简化逻辑):java// PendingIntentRecord 是 AMS 内部存储 PendingIntent 的核心类 class PendingIntentRecord { final Key key; // 包含 creator UID, 类型(Activity/Broadcast/Service), requestCode, 目标包名等 final Intent intent; // 这是深拷贝存储的原始 Intent 对象!!!!!! ... } // 当调用 PendingIntent.send() 时 void send(...) { // AMS 找到对应的 PendingIntentRecord PendingIntentRecord rec = findPendingIntentLocked(...); // 核心:取出存储的 *原始* Intent 对象 'rec.intent' Intent intent = rec.intent; // 可能会合并一些 send() 调用时传入的 extra (如果设置了 FLAG_UPDATE_CURRENT 等) ... // 然后 AMS 用这个(基本是原始的)Intent 去启动 Activity/发送 Broadcast/启动 Service startActivityLocked(...intent...); // 或 broadcastIntentLocked, startServiceLocked }
-
覆盖安装的影响: App A 从 1.0 升级到 2.0 时,它的 UID 通常保持不变 。AMS 中存储的
PendingIntentRecord
的Key
(包含 UID) 依然有效。当 App B 再次调用send()
时,AMS 成功找到这个旧记录,并取出里面存储的App A 1.0 时期创建的那个Intent
。这个Intent
很可能显式指定了旧版登录页的组件名 (如com.example.appA.v1.LoginActivity
),或者即使使用 Action,其内部的 flags、categories、data 等也是 1.0 时期的配置。系统会严格按照这个旧的Intent
要求去寻找目标组件。如果旧版组件在覆盖安装后还没有被完全清理(或者新的 2.0 版本没有声明完全相同的 Intent Filter 来覆盖旧的),系统就可能启动那个残留的 1.0 组件。
-
-
隐式 Intent 解析 - "实时寻人":
-
当 App B 使用
startActivity(new Intent(SettingsActivity.ACTION_SHOW_SETTINGS))
这样的隐式 Intent 时,系统(PackageManagerService + AMS)会在调用发生的当下 ,去查询当前所有已安装应用 中注册的IntentFilter
。 -
核心机制: 这是一个实时解析 的过程。它基于当前安装的最新 App A 2.0 的
AndroidManifest.xml
中声明的组件和它们的 Intent Filter。 -
关键源码 (
PackageManagerService
/ActivityManagerService
- 简化逻辑):java// AMS 处理 startActivity (隐式) int startActivityLocked(...Intent intent...) { // 将隐式 Intent 发送给 PMS 进行解析 ResolveInfo rInfo = AppGlobals.getPackageManager().resolveIntent(...intent...); if (rInfo != null) { // rInfo.activityInfo 包含了 PMS 找到的 *当前最佳匹配* 的 Activity 信息 (来自最新安装的 App A 2.0) ActivityInfo aInfo = rInfo.activityInfo; // 使用这个最新的 ActivityInfo 来启动 Activity startActivityUncheckedLocked(...aInfo...); } } // PMS 的 resolveIntent 会查询当前的 Package 数据库 public ResolveInfo resolveIntent(...Intent intent...) { List<ResolveInfo> query = queryIntentActivities(...intent...); // 查询所有匹配的 Activity return chooseBestActivity(...query...); // 根据优先级等规则选出一个最佳的 }
-
覆盖安装的影响: App A 升级到 2.0 后,它的
AndroidManifest.xml
被更新。新的SettingsActivity_v2
声明了相应的 Intent Filter (Action/Category等)。当 App B 使用隐式 Intent 请求设置页时,PMS 查询到的是当前 App A 2.0 中声明的最新SettingsActivity
。因此启动的自然是新版本。
-
总结:痴情的"PendingIntent"与现实的"隐式Intent"
- PendingIntent (
老王的情书盒子
): 它代表的是一个过去的承诺 ,一个固化的执行指令 。它创建时是什么样子(目标组件是什么),执行时就努力还原成什么样子(启动那个旧组件)。它活在创建它的那一刻(App A 1.0 安装时)。覆盖安装后,只要这个"盒子"还在被使用(IntentSender
没失效),它就会固执地召唤旧梦(旧版组件)。它的"痴情"在于它对创建时状态的绝对忠诚。 - 隐式 Intent (
实时寻人启事
): 它代表的是当下的需求 。它不关心过去,只关心现在谁能满足这个需求(匹配这个 Intent Filter)。它会根据系统当前最新的状态 (所有已安装应用的最新清单)去寻找最佳匹配者。它的"现实"在于它总是拥抱当前最新的组件。
解决方案:告别旧梦,迎接新颜
要让登录页也显示新版本,必须让那个"痴情的旧情书盒子"(旧的 PendingIntent)失效,或者让它指向新的地址:
- 主动取消旧情书 (推荐): 在 App A 的后台任务(如处理通知或定时任务)完成 或 App A 升级后首次启动 时,调用
PendingIntent.cancel()
让老王销毁那个旧盒子。这样媒婆小B下次再想找老王要那个盒子时,老王会说"没了",小B(App B)就需要重新创建一个新的 PendingIntent(指向 App A 2.0 的新登录页组件)。 - 使用动态特征的情书: 在创建 PendingIntent 时,在
Intent
中使用 Action 而不是显式组件名,并确保 App A 2.0 的登录页也声明了完全相同 的 Action。同时,在创建 PendingIntent 时加上标志FLAG_UPDATE_CURRENT
。这样,当 App B 调用send()
时,AMS 会用 App B 传入的 Intent (如果有) 去更新 存储的旧 Intent 中的 extra data 。但是!这通常 不会 改变目标组件本身! 它主要更新的是Intent
里的extras
。如果旧 Intent 是显式指定组件的,这个标志也无法改变目标组件。所以这招对组件变更效果有限,不如方案1可靠。 - 赋予情书可变性 (谨慎): 在创建 PendingIntent 时使用
FLAG_MUTABLE
。这允许 App B 在send()
时传入一个新的 Intent,这个新 Intent 可以完全覆盖 旧的 Intent(包括目标组件)。风险: 这引入了安全漏洞(PendingIntent 劫持),需要非常小心地设计和验证传入的 Intent。一般不推荐。 - 让旧地址彻底消失: 确保 App A 2.0 的
AndroidManifest.xml
完全覆盖 了旧版登录页的 Intent Filter。如果旧版登录页使用隐式 Intent 启动,新版登录页声明了相同或更高优先级的相同 Intent Filter,系统就会优先选择新版。如果旧版是显式启动,则此方法无效。 - 改变信物标识: 在创建 PendingIntent 时使用
requestCode
。在 App A 2.0 中,使用一个不同的requestCode
来创建指向新登录页的 PendingIntent。这样在 App B 端,它请求的是一个新的"盒子"(新的IntentSender
),指向的就是新组件。这需要 App B 配合更新它请求的 PendingIntent。
最可靠、最安全的方案是方案1:在适当的时机(如任务完成、用户登出、应用升级后)主动取消不再需要的旧 PendingIntent。 就像告诉老王:"那封旧情书作废了,请销毁它吧。" 这样,当需要新的开始(启动登录页)时,就只能创建指向新地址(新组件)的新情书了。