PengdingIntent之“我想要的很简单时光还在你还在”

让我们来剖析这个"痴情旧梦"的Android覆盖安装之谜,用一段故事结合源码原理讲清楚。

故事篇:时光邮差与不变的情书

想象一个叫安卓镇的地方,镇上有个痴情的书生叫小程 。他深爱着镇上的姑娘小A

  1. 初遇与情书 (App A 1.0 安装):

    • 小程第一次见到小A (安装 App A 1.0)。
    • 他为小A写了一封炽热的情书,请求她做他的女朋友(这封情书就是 PendingIntent)。情书里详细写明了小A家的地址(Intent 目标组件:登录页 LoginActivity_v1)。
    • 小程把这封情书交给了镇上的老邮差老王 (系统服务 ActivityManagerService - AMS)。老王有个习惯:他不会记住小A这个人本身,而是牢牢记住这封情书的内容和小程委托他时的场景(老王代表 PendingIntent 的底层机制)。
    • 老王把这封情书小心翼翼地锁在了自己专属的铜盒子里(IntentSender),盒子上贴着标签:"小程给地址X的情书"。老王只认盒子里的情书内容和这个标签。
  2. 媒人牵线 (App B 触发):

    • 镇上的热心媒婆小B(App B)想撮合小程和小A。
    • 小B知道老王那里有小程给小A的情书盒子。她找到老王说:"老王,请把小程给小A的那封情书盒子给我,我要亲自交给小A!"(App B 调用 PendingIntent.send(),本质是获取 IntentSender 并请求 AMS 执行里面的 Intent)。
    • 关键点来了:老王只认盒子(IntentSender)! 他拿出那个铜盒子,看着里面封存的情书内容:"送给地址X的小A"。他完全按照情书里写的原始地址 ,把这封情书送到了地址X
  3. 小A的改变 (App A 2.0 覆盖安装):

    • 时间流逝,小A成长了(App A 升级到 2.0)。她搬了新家(代码重构),换了新衣服(UI改版),甚至改了名字(LoginActivity_v1 可能变成了 LoginActivity_v2,或者逻辑大改)。她的新家在地址Y。
    • 但是!老王盒子里的那封旧情书,地址写的还是旧的地址X! 老王对此毫不知情 ,也不关心小A现在具体是谁、住哪里。他只认盒子里的原始指令。
  4. 媒人再牵线与旧梦重现 (登录页旧版):

    • 媒婆小B(App B)又一次想帮忙,她再次找到老王 ,请求把"小程给小A的那封情书盒子"给她(再次调用 同一个 PendingIntent.send())。
    • 老王依然只认那个铜盒子(IntentSender)。他原封不动地 再次拿出旧情书,看到地址"X",于是又一次把情书送到了地址X
    • 住在地址X的是谁?是小A的1.0版本(旧版 LoginActivity)! 她虽然已经"升级"了,但安卓镇有个神奇的现象:旧版本的她(旧的Activity组件)并没有完全消失,只要还有人记得她的地址(Intent 匹配),她就能被唤醒(系统可能保留旧APK的部分信息直到进程重启)。于是,小程看到的,还是那个他最初爱上的、旧模样的小A(App A 1.0 的登录页)。
  5. 新颜相见 (设置页新版):

    • 媒婆小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 解析) 的行为差异:

  1. 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 中存储的 PendingIntentRecordKey (包含 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 组件。

  2. 隐式 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)失效,或者让它指向新的地址:

  1. 主动取消旧情书 (推荐): 在 App A 的后台任务(如处理通知或定时任务)完成 或 App A 升级后首次启动 时,调用 PendingIntent.cancel() 让老王销毁那个旧盒子。这样媒婆小B下次再想找老王要那个盒子时,老王会说"没了",小B(App B)就需要重新创建一个新的 PendingIntent(指向 App A 2.0 的新登录页组件)。
  2. 使用动态特征的情书: 在创建 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可靠。
  3. 赋予情书可变性 (谨慎): 在创建 PendingIntent 时使用 FLAG_MUTABLE。这允许 App B 在 send() 时传入一个新的 Intent,这个新 Intent 可以完全覆盖 旧的 Intent(包括目标组件)。风险: 这引入了安全漏洞(PendingIntent 劫持),需要非常小心地设计和验证传入的 Intent。一般不推荐。
  4. 让旧地址彻底消失: 确保 App A 2.0 的 AndroidManifest.xml 完全覆盖 了旧版登录页的 Intent Filter。如果旧版登录页使用隐式 Intent 启动,新版登录页声明了相同或更高优先级的相同 Intent Filter,系统就会优先选择新版。如果旧版是显式启动,则此方法无效。
  5. 改变信物标识: 在创建 PendingIntent 时使用 requestCode。在 App A 2.0 中,使用一个不同的 requestCode 来创建指向新登录页的 PendingIntent。这样在 App B 端,它请求的是一个新的"盒子"(新的 IntentSender),指向的就是新组件。这需要 App B 配合更新它请求的 PendingIntent。

最可靠、最安全的方案是方案1:在适当的时机(如任务完成、用户登出、应用升级后)主动取消不再需要的旧 PendingIntent。 就像告诉老王:"那封旧情书作废了,请销毁它吧。" 这样,当需要新的开始(启动登录页)时,就只能创建指向新地址(新组件)的新情书了。

相关推荐
猪哥帅过吴彦祖3 分钟前
Flutter SizeTransition:让你的UI动画更加丝滑
android·flutter
OperateCode1 小时前
Android Studio 格式规范
android
张风捷特烈1 小时前
鸿蒙纪·Flutter卷#02 | 已有 Flutter 项目鸿蒙化 · 3.27.4 版
android·flutter·harmonyos
QING6184 小时前
Media3 ExoPlayer 快速实现背景视频播放(干货)
android·前端·kotlin
weiwuxian4 小时前
js与原生通讯版本演进
android·前端
wayne2144 小时前
Android 跨应用广播通信全攻略
android
叽哥4 小时前
flutter学习第 12 节:网络请求与 JSON 解析
android·flutter·ios
y东施效颦5 小时前
uni-app app端安卓和ios如何申请麦克风权限,唤起提醒弹框
android·ios·uni-app
亲爱的非洲野猪6 小时前
从 0 到 1:用 MyCat 打造可水平扩展的 MySQL 分库分表架构
android·mysql·架构