将用通俗易懂的“快递站分拣入库”故事并结合代码,详细讲解这篇关于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_COPYjava 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())。
- a. 拆包深度检查: 管理员用专业的扫描仪(
-
代码 (关键片段):
-
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)在安装完成后,也会清理自己留下的垃圾(比如复制失败时删除暂存区)。
- 如果一个包裹成功入库(或失败被退回),调度员(PackageHandler)会把它从“待处理包裹区”(
-
代码: (在
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/目录下的特定文件中。
通俗总结流程:
-
任务下达: 前台(PackageInstaller)说“有包裹要安装”(
installStage()),给调度员(PackageHandler)发消息(INIT_COPY)和任务单(InstallParams)。 -
叫分拣车: 调度员发现车没来(
!mBound),打电话叫车(connectToService()),任务单排队(mPendingInstalls.add())。车来了会通知调度员(onServiceConnected()->MCS_BOUND消息)。 -
开始分拣: 调度员拿到车钥匙(
mContainerService),让队首任务单开工(startCopy())。 -
轻检 & 复制: 任务单用车做快速检查(
getMinimalPackageInfo()),决定放哪,然后让车把包裹复制到暂存区(FileInstallArgs.doCopyApk()->imcs.copyPackage())。 -
申请入库: 复制完成,任务单告诉调度员“我干完了”。调度员通知仓库总管(PMS)“这个包裹可以正式入库了”(
processPendingInstall())。 -
深度检查 & 入库: 总管亲自出马:
- 深度扫描包裹内容(
PackageParser.parsePackage())。 - 给暂存区换正式名字(
args.doRename())。 - 关键! 如果是更新,严查签名是否一致(
checkUpgradeKeySetLP())。不一致就拒收! - 检查特殊规则(如系统App不能放SD卡)。
- 正式扫描入库,注册App信息到系统(
scanPackageTracedLI())。 - 更新总账本(
updateSettingsLI())。 - 给新App准备用户小仓库(
prepareAppDataAfterInstallLIF())。
- 深度扫描包裹内容(
-
清理 & 下一个: 搞定一个包裹,从队列移除。队列空了?等10秒让车走(
MCS_UNBIND)。还有货?立刻叫调度员处理下一个(sendEmptyMessage(MCS_BOUND))。
通过这个故事和代码结合的解释,相信你对Android系统内部处理APK安装的核心流程——特别是PMS如何通过PackageHandler驱动、如何利用DefaultContainerService跨进程复制文件、以及如何进行关键的签名验证和深度扫描注册——有了更清晰和深入的理解。这个过程设计精妙,充分考虑了性能(异步、跨进程)、安全性(签名验证)和模块化(不同服务职责分离)。