浅谈PMS处理APK安装

71 阅读11分钟

将用通俗易懂的“快递站分拣入库”故事并结合代码,详细讲解这篇关于PMS处理APK安装的文章。

​核心故事梗概:​

想象一个巨大的安卓仓库(Android系统),所有App就是里面的货物(APK)。用户下载了一个新App(APK),这就像用户在网上买了个新包裹。PackageInstaller(快递员)把包裹送到了仓库大门口(把APK的信息传递给PMS)。现在,仓库管理员PMS(PackageManagerService)和他的得力助手PackageHandler(分拣调度员)要负责把这个新包裹拆开检查、分拣、登记入库、放到正确的货架上。这篇文章讲的就是仓库管理员(PMS)和调度员(PackageHandler)如何处理这个包裹(APK)的具体过程。

​详细分解:​

​1. 快递员送达,仓库收到任务单 (PackageInstaller -> PMS)​

  • ​故事:​​ PackageInstaller 把包裹(APK)的基本信息(包名、路径、安装参数等)打包成一张“任务单”(InstallParams),递给了仓库前台(PMS)。

  • ​代码:​​ installStage() 方法

    java
    Copy
    void installStage(...) {
        final Message msg = mHandler.obtainMessage(INIT_COPY); // 1. 写张纸条:“有包裹待处理”
        final InstallParams params = new InstallParams(...); // 2. 创建任务单(InstallParams),包含包裹详情
        msg.obj = params; // 3. 把任务单别在纸条上
        mHandler.sendMessage(msg); // 4. 把纸条给调度员(PackageHandler)
    }
    
    • INIT_COPY 消息类型:表示初始化复制操作。
    • InstallParams:包裹的任务单,包含所有安装所需信息(来源路径、安装位置、安装标志、安装原因、权限、证书等)。

​2. 调度员接手,准备分拣工具 (PackageHandler处理 INIT_COPY 消息)​

  • ​故事:​​ 调度员(PackageHandler)收到“有包裹待处理”(INIT_COPY)的纸条。他发现仓库里用于拆包检查的专用分拣车(DefaultContainerService)还没准备好(mBound = false)。他赶紧打电话给分拣中心(com.android.defcontainer 进程),要求派一辆分拣车过来(bindServiceAsUser)。同时,他把这个新包裹的任务单(InstallParams)放到仓库门口的“待处理包裹区”(mPendingInstalls队列)排队。

  • ​代码:​​ doHandleMessage() 处理 INIT_COPY

    java
    Copy
    case INIT_COPY: {
        HandlerParams params = (HandlerParams) msg.obj; // 拿到任务单
        if (!mBound) { // 检查分拣车是否已就位(mBound)
            if (!connectToService()) { // 打电话叫分拣车(绑定服务)
                params.serviceError(); // 叫车失败,通知任务单出错
                return;
            } else {
                mPendingInstalls.add(idx, params); // 叫车成功,任务单加入待处理队列
            }
        } else {
            mPendingInstalls.add(idx, params); // 车已在,直接加入队列
            if (idx == 0) { // 如果是队列里第一个任务
                mHandler.sendEmptyMessage(MCS_BOUND); // 立刻发消息:“车已到,可以开工了!”
            }
        }
        break;
    }
    
    • connectToService():绑定到 DefaultContainerService,成功后设置 mBound = true
    • mPendingInstalls:一个 ArrayList<HandlerParams>,存储等待处理的安装任务单。

​3. 分拣车到位,调度员通知开工 (PackageHandler处理 MCS_BOUND 消息)​

  • ​故事:​​ 分拣车(DefaultContainerService)开到了仓库(服务绑定成功)。分拣中心通过电话线(Binder IPC)告诉调度员:“车到了!”(调用 onServiceConnected())。调度员收到通知后,写了一张新的纸条“分拣车已到”(MCS_BOUND),并把电话线(IMediaContainerService)也附在了纸条上(msg.obj = service)。调度员拿起“待处理包裹区”最上面那个任务单(mPendingInstalls.get(0)),告诉任务单:“车来了,你可以开始干活了!”(调用 params.startCopy())。

  • ​代码:​

    • onServiceConnected() (在 DefaultContainerConnection 内部类中)

      java
      Copy
      public void onServiceConnected(ComponentName name, IBinder service) {
          final IMediaContainerService imcs = ...; // 获得分拣车操作手柄(IPC接口)
          mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs)); // 发纸条(MCS_BOUND),附上手柄(imcs)
      }
      
    • doHandleMessage() 处理 MCS_BOUND (简化关键部分)

      java
      Copy
      case MCS_BOUND: {
          if (msg.obj != null) {
              mContainerService = (IMediaContainerService) msg.obj; // 拿到分拣车操作手柄(IMediaContainerService)
          }
          if (mContainerService != null && mPendingInstalls.size() > 0) {
              HandlerParams params = mPendingInstalls.get(0); // 取队列第一个任务单
              if (params.startCopy()) { // 告诉任务单:“开始干活(复制包裹)!”
                  ... // 处理成功后续(稍后讲)
              }
          }
          break;
      }
      
    • mContainerService:PMS持有的 IMediaContainerService 接口,用于跨进程调用 DefaultContainerService 的功能。

​4. 任务单开始工作:轻量检查 & 复制包裹到暂存区 (InstallParams.handleStartCopy() & FileInstallArgs.doCopyApk())​

  • ​故事:​​ 任务单(InstallParams)收到调度员“开始干活”的指令(startCopy())。它首先用分拣车(mContainerService)对包裹(APK)做一个快速的“初步检查”(getMinimalPackageInfo()),主要是看看包裹大小、推荐的存放位置(仓库内部Data区、SD卡区、还是临时快闪区?)。接着,任务单根据初步检查结果,决定最终把包裹放在仓库的哪个大区(创建对应的 InstallArgs,比如 FileInstallArgs 对应内部存储)。任务单命令分拣车(mContainerService)把包裹从原始位置(比如下载目录)复制到仓库内部的“暂存区”(一个临时文件夹,如 /data/app/vmdl18300388.tmp/)。复制完成后,任务单告诉调度员:“我的复制活干完了!”。

  • ​代码:​

    • startCopy() (在 HandlerParams 类)

      java
      Copy
      final boolean startCopy() {
          ... // 尝试次数检查略
          try {
              handleStartCopy(); // 核心工作交给子类(InstallParams)实现
              res = true;
          } catch (...) { ... }
          handleReturnCode(); // 复制完成后,处理返回码(进入安装阶段)
          return res;
      }
      
    • handleStartCopy() (在 InstallParams 类)

      java
      Copy
      public void handleStartCopy() throws RemoteException {
          // 1. 确定最终存放区域 (onSd? onInt? ephemeral?)
          ... // 处理安装位置冲突逻辑略
          // 2. 轻量检查包裹 (调用分拣车)
          pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags, packageAbiOverride);
          ... // 处理检查结果错误码略
          // 3. 根据位置创建具体的搬运工(InstallArgs)
          final InstallArgs args = createInstallArgs(this); // 通常是FileInstallArgs
          mArgs = args;
          ... // 其他检查略
          // 4. 命令搬运工(InstallArgs)调用分拣车复制包裹到暂存区!
          ret = args.copyApk(mContainerService, true);
          mRet = ret; // 记录复制结果
      }
      
    • doCopyApk() (在 FileInstallArgs 类 - copyApk() 的核心实现)

      java
      Copy
      private int doCopyApk(IMediaContainerService imcs, boolean temp) throws RemoteException {
          File tempDir = mInstallerService.allocateStageDirLegacy(volumeUuid, isEphemeral); // 1. 申请暂存区(/data/app/vmdlXXXXXX.tmp)
          ...
          ret = imcs.copyPackage(origin.file.getAbsolutePath(), target); // 2. 跨进程调用分拣车复制文件到暂存区!
          return ret;
      }
      

​5. 包裹入库登记:深度检查 & 正式上架 (processPendingInstall() & installPackageLI())​

  • ​故事:​​ 调度员(PackageHandler)看到任务单(InstallParams)的复制活干完了(startCopy()返回),就让它去处理一下“入库结果”(handleReturnCode())。任务单于是通知仓库管理员(PMS):“有个包裹复制到暂存区了,现在需要正式入库安装啦!”(调用 processPendingInstall(mArgs, mRet))。仓库管理员(PMS)亲自出马:

    • ​a. 拆包深度检查:​​ 管理员用专业的扫描仪(PackageParser)仔细扫描包裹(APK)里的所有内容(parsePackage()),解析出里面的清单文件(AndroidManifest.xml),弄清楚这个App叫什么(包名)、需要什么权限、由哪些组件构成等等。
    • ​b. 包裹重命名:​​ 管理员把暂存区的临时文件夹(vmdl18300388.tmp)重命名为正式的、带版本号的仓库货架名(例如 /data/app/包名-1/),里面的文件也重命名(如 base.apk)。
    • ​c. 安全检查 (替换安装时):​​ 如果仓库里​​已经​​有同名的App(老版本),管理员会非常仔细地核对两个包裹(新APK和老APK)的“发货人签名”(证书)。只有同一个“发货人”(开发者)发来的、签名一致的包裹,才能替换旧的。如果签名不一致,管理员会拒绝入库(INSTALL_FAILED_UPDATE_INCOMPATIBLE)。
    • ​d. 位置限制检查:​​ 管理员还会检查一些特殊规则。比如,仓库自带的系统App(预装App)​​不能​​被放到SD卡区,也不能被临时快闪App替换。
    • ​e. 正式入库扫描:​​ 管理员指挥扫描仪(scanPackageTracedLI())对正式货架上的APK进行最终的深度扫描,将解析出的所有信息(组件、权限、资源等)登记到仓库的核心数据库(mPackages)中。
    • ​f. 更新仓库账本:​​ 管理员更新仓库的总账本(Settings),记录下这个新App安装给了哪个用户、是谁安装的(安装来源)等信息(updateSettingsLI())。
    • ​g. 准备用户数据:​​ 管理员为新App在用户数据区创建专属的数据文件夹(/data/user/0/包名/),用于存放该App的私有设置、数据库等(prepareAppDataAfterInstallLIF())。
  • ​代码 (关键片段):​

    • handleReturnCode() -> processPendingInstall()

      java
      Copy
      void handleReturnCode() {
          if (mArgs != null) {
              processPendingInstall(mArgs, mRet); // 进入安装阶段
          }
      }
      private void processPendingInstall(...) {
          mHandler.post(() -> {
              args.doPreInstall(...); // 安装前准备(主要是环境检查)
              synchronized (mInstallLock) {
                  installPackageTracedLI(args, res); // 核心安装逻辑(加锁保证原子性)
              }
              args.doPostInstall(...); // 安装后清理(成功则收尾,失败则删除临时文件)
          });
      }
      
    • installPackageLI() (非常长,展示关键步骤)

      java
      Copy
      private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
          PackageParser pp = new PackageParser(); // 创建扫描仪
          // 1. 深度解析APK (在暂存区)
          final PackageParser.Package pkg = pp.parsePackage(tmpPackageFile, parseFlags);
          ...
          // 2. 检查是否替换安装
          if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
              ... // 处理包名重定向等
              // 3. 安全检查:签名验证 (关键
              if (shouldCheckUpgradeKeySetLP(signatureCheckPs, scanFlags)) {
                  if (!checkUpgradeKeySetLP(signatureCheckPs, pkg)) {
                      res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "签名不匹配!");
                      return;
                  }
              }
              ... // 其他替换相关逻辑
          }
          // 4. 位置限制检查 (如系统App不能在SD卡更新)
          if (systemApp) {
              if (onExternal) { ... error ... }
              else if (instantApp) { ... error ... }
          }
          // 5. 重命名暂存区 -> 正式目录 (/data/app/包名-1/)
          if (!args.doRename(res.returnCode, pkg, oldCodePath)) { ... error ... }
          // 6. 核心扫描:正式目录深度扫描 & 注册组件到系统
          try {
              if (replace) {
                  replacePackageLIF(pkg, ...); // 替换安装流程 (内部会调用扫描)
              } else {
                  PackageParser.Package newPackage = scanPackageTracedLI(pkg, ...); // 新安装扫描
                  // 7. 更新Settings (账本)
                  updateSettingsLI(newPackage, ...);
                  // 8. 准备用户数据
                  if (res.returnCode == INSTALL_SUCCEEDED) {
                      prepareAppDataAfterInstallLIF(newPackage);
                  }
              }
          } catch (...) { ... }
      }
      

​6. 清理收尾 & 处理下一个包裹​

  • ​故事:​

    • 如果一个包裹成功入库(或失败被退回),调度员(PackageHandler)会把它从“待处理包裹区”(mPendingInstalls)的队首移除。
    • 如果队列空了,调度员会等一会儿(10秒延迟),然后发个消息(MCS_UNBIND)让那辆分拣车(DefaultContainerService)可以开回去了(解绑服务,节省资源)。如果队列里还有包裹,调度员立刻再发一张“分拣车已到”(MCS_BOUND)的纸条,通知自己处理下一个包裹。
    • 任务单(InstallParams)在安装完成后,也会清理自己留下的垃圾(比如复制失败时删除暂存区)。
  • ​代码:​​ (在 MCS_BOUND 消息处理的后半段)

    java
    Copy
    if (params.startCopy()) { // 如果复制&安装任务成功完成
        if (mPendingInstalls.size() > 0) {
            mPendingInstalls.remove(0); // 移除完成的任务
        }
        if (mPendingInstalls.size() == 0) { // 如果队列空了
            if (mBound) {
                sendMessageDelayed(obtainMessage(MCS_UNBIND), 10000); // 10秒后发解绑消息
            }
        } else {
            mHandler.sendEmptyMessage(MCS_BOUND); // 立刻处理下一个任务!
        }
    }
    
    • MCS_UNBIND 消息最终会调用 unbindService() 解绑 DefaultContainerService

​关键角色总结:​

  • ​PMS (PackageManagerService):​​ 仓库总管。负责核心安装逻辑(深度解析、签名验证、数据库更新、用户数据准备)、卸载、查询App信息等。是Binder服务的提供者。
  • ​PackageHandler:​​ PMS内部的调度员/工头。负责接收外部请求(消息)、绑定/解绑分拣车服务、管理任务队列、驱动各个安装步骤的执行(复制->安装)。它运行在PMS的主线程(通常是system_server进程的主线程)。
  • ​DefaultContainerService (IMediaContainerService):​​ 专业的包裹分拣中心/分拣车。运行在独立的 com.android.defcontainer 进程。负责对APK文件进行轻量级解析(获取基本信息)和最重要的文件复制操作(从源位置到PMS指定的暂存区)。它的存在是为了避免耗时的文件操作阻塞PMS的主线程。
  • ​InstallParams:​​ 单个安装任务的任务单。封装了特定APK安装的所有信息(路径、参数等)和核心处理逻辑(handleStartCopy()handleReturnCode())。
  • ​InstallArgs (FileInstallArgs/AsecInstallArgs):​​ 搬运工。根据安装位置创建,具体执行文件操作(申请暂存区、调用分拣车复制、重命名目录到正式位置)。
  • ​PackageParser:​​ 扫描仪。负责深度解析APK文件,提取出包名、组件、权限、资源等核心信息。
  • ​Settings:​​ 仓库的总账本。保存所有包的动态设置信息(安装位置、安装来源、权限授予状态、用户关联等)。存储在 /data/system/ 目录下的特定文件中。

​通俗总结流程:​

  1. ​任务下达:​​ 前台(PackageInstaller)说“有包裹要安装”(installStage()),给调度员(PackageHandler)发消息(INIT_COPY)和任务单(InstallParams)。

  2. ​叫分拣车:​​ 调度员发现车没来(!mBound),打电话叫车(connectToService()),任务单排队(mPendingInstalls.add())。车来了会通知调度员(onServiceConnected() -> MCS_BOUND消息)。

  3. ​开始分拣:​​ 调度员拿到车钥匙(mContainerService),让队首任务单开工(startCopy())。

  4. ​轻检 & 复制:​​ 任务单用车做快速检查(getMinimalPackageInfo()),决定放哪,然后让车把包裹复制到暂存区(FileInstallArgs.doCopyApk() -> imcs.copyPackage())。

  5. ​申请入库:​​ 复制完成,任务单告诉调度员“我干完了”。调度员通知仓库总管(PMS)“这个包裹可以正式入库了”(processPendingInstall())。

  6. ​深度检查 & 入库:​​ 总管亲自出马:

    • 深度扫描包裹内容(PackageParser.parsePackage())。
    • 给暂存区换正式名字(args.doRename())。
    • ​关键!​​ 如果是更新,严查签名是否一致(checkUpgradeKeySetLP())。不一致就拒收!
    • 检查特殊规则(如系统App不能放SD卡)。
    • 正式扫描入库,注册App信息到系统(scanPackageTracedLI())。
    • 更新总账本(updateSettingsLI())。
    • 给新App准备用户小仓库(prepareAppDataAfterInstallLIF())。
  7. ​清理 & 下一个:​​ 搞定一个包裹,从队列移除。队列空了?等10秒让车走(MCS_UNBIND)。还有货?立刻叫调度员处理下一个(sendEmptyMessage(MCS_BOUND))。

通过这个故事和代码结合的解释,相信你对Android系统内部处理APK安装的核心流程——特别是PMS如何通过PackageHandler驱动、如何利用DefaultContainerService跨进程复制文件、以及如何进行关键的签名验证和深度扫描注册——有了更清晰和深入的理解。这个过程设计精妙,充分考虑了性能(异步、跨进程)、安全性(签名验证)和模块化(不同服务职责分离)。