文章内容
- 一、安装入口:点开 APK 后系统处理了什么
- 1.1 确认界面属于 PackageInstaller
- 1.2 URI 与 scheme 检查
- 二、发起安装:触发跨进程调用
- 2.1 InstallAppProgress 传递的参数
- 2.2 observer 为什么是 Binder 服务端
- 2.3 ApplicationPackageManager 转入 IPackageManager
- 三、PMS 收到请求:绑定 DefaultContainerService
- 3.1 请求先进入消息队列
- 3.2 mBound 与 mPendingInstalls
- 3.3 MCSBOUND、失败与重试
- 四、第一步:把 APK 复制到 /data/app
- 4.1 从 startCopy 到 copyApk
- 4.2 临时目录与目标文件
- 4.3 DefaultContainerService 搬运字节
- 五、第二步:解析 AndroidManifest.xml 并写入 mPackages
- 5.1 processPendingInstall 的三段处理
- 5.2 PackageParser 读取 APK
- 5.3 mPackages 是运行期索引
- 六、第三步:把结果送回发起进程
- 6.1 POSTINSTALL 负责系统侧收尾
- 6.2 observer 把 returnCode 送回界面
- 七、把四步连成一条链
- 八、小结与动手验证
本文探讨的问题是:用户在文件管理器中点开一个 APK 后,系统怎样把它放进 /data/app、解析包信息,并把安装结果送回界面?从源码看,这条链横跨 PackageInstaller 应用进程、system_server 和 com.android.defcontainer 三个进程。
一、安装入口:点开 APK 后系统处理了什么
1.1 确认界面属于 PackageInstaller
文件管理器只负责发出打开 APK 的 Intent。权限确认页和安装进度页来自系统的 PackageInstaller 应用,不属于文件管理器,也不属于待安装 APK。PackageInstallerActivity 接收请求后读取 URI,准备包信息与权限列表;用户确认安装后,界面转到 InstallAppProgress。
PackageInstallerActivity 的清单声明给出了入口依据。VIEW 配合 APK MIME 类型接收 file URI,INSTALL_PACKAGE 接收 file 与 package 两种 scheme,系统因此能把文件管理器或下载管理器发出的打开 APK 请求派给它;CONFIRM_PERMISSIONS 对应 1.2 的会话确认分支。InstallAppProgress 设置为 android:exported="false",外部应用不能直接启动进度页,确认页会在用户同意后从应用内部转入该界面。
<!-- 声明权限确认与安装入口所在的 Activity --><activity android:name=".PackageInstallerActivity" <!-- 屏幕旋转和屏幕尺寸变化时不重建界面 --> android:configChanges="orientation|keyboardHidden|screenSize" <!-- 不进入最近任务列表 --> android:excludeFromRecents="true"> <!-- 处理打开 APK 文件与显式安装请求 --> <intent-filter> <!-- 响应查看动作 --> <action android:name="android.intent.action.VIEW" /> <!-- 响应安装动作 --> <action android:name="android.intent.action.INSTALL_PACKAGE" /> <!-- 使用默认分类 --> <category android:name="android.intent.category.DEFAULT" /> <!-- 匹配 file 的形式 --> <data android:scheme="file" /> <!-- 匹配 APK 的 MIME 类型 --> <data android:mimeType="application/vnd.android.package-archive" /> <!-- 结束打开 APK 请求过滤器 --> </intent-filter> <!-- 处理显式安装请求的 file 与 package 两种形式 --> <intent-filter> <!-- 响应安装动作 --> <action android:name="android.intent.action.INSTALL_PACKAGE" /> <!-- 使用默认分类 --> <category android:name="android.intent.category.DEFAULT" /> <!-- 匹配 file 的形式 --> <data android:scheme="file" /> <!-- 匹配已安装包名的形式 --> <data android:scheme="package" /> <!-- 结束显式安装请求过滤器 --> </intent-filter> <!-- 处理安装会话的权限确认请求 --> <intent-filter> <!-- 响应权限确认动作 --> <action android:name="android.content.pm.action.CONFIRM_PERMISSIONS" /> <!-- 使用默认分类 --> <category android:name="android.intent.category.DEFAULT" /> <!-- 结束权限确认请求过滤器 --> </intent-filter><!-- 结束权限确认 Activity 的声明 --></activity> <!-- 声明安装进度页 --><activity android:name=".InstallAppProgress" <!-- 屏幕旋转和屏幕尺寸变化时不重建界面 --> android:configChanges="orientation|keyboardHidden|screenSize" <!-- 仅允许应用自身启动 --> android:exported="false" />apk安装过程会出现“正在检查”、安装确认和安装进度三类界面。前两类界面需要先读取 APK 的轻量信息,用于显示图标、应用名、安装来源、权限,以及判断当前操作是新装还是覆盖安装。此时还没有把完整的 PackageParser.Package 写入 PMS 的 mPackages。
这个区分能避免把“显示安装信息”和“完成包扫描”混成同一阶段。确认页必须在用户同意前给出足够的信息,但 PMS 的正式安装还没有开始;用户取消时,也不应在 /data/app 留下一个已经登记的包。界面侧的读取服务于交互,PMS 侧的解析服务于系统包索引,两者调用的解析能力相近,写入状态的时机不同。
1.2 URI 与 scheme 检查
PackageInstallerActivity.onCreate 从启动 Intent 中取得待处理地址。会话确认分支从 SessionInfo.resolvedBaseCodePath 还原文件 URI,普通分支则使用 Intent.getData()。Android 6.0 这里接受 file 和 package 两种 scheme,其余 scheme 会以 INSTALL_FAILED_INVALID_URI 结束。
// 取得启动当前界面的 Intentfinal Intent intent = getIntent();// 判断请求是否来自安装会话的权限确认动作if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) { // 读取安装会话编号 final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1); // 查询对应的会话信息 final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId); // 会话不存在、未封版或缺少基础代码路径时视为异常状态 if (info == null || !info.sealed || info.resolvedBaseCodePath == null) { // 记录会话状态异常 Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring"); // 结束当前界面 finish(); // 终止 onCreate 的后续处理 return; // 结束会话状态判断 } // 记录当前处理的会话编号 mSessionId = sessionId; // 根据已经解析出的基础代码路径构造文件 URI mPackageURI = Uri.fromFile(new File(info.resolvedBaseCodePath)); // 会话确认场景不携带来源 URI mOriginatingURI = null; // 会话确认场景不携带 referrer mReferrerURI = null;// 处理普通的 APK 打开请求} else { // 普通请求不关联安装会话 mSessionId = -1; // 从 Intent 数据区读取 APK URI mPackageURI = intent.getData(); // 读取请求的来源 URI mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); // 读取请求的引用 URI mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);// 结束请求来源分支}// 读取 URI 的 schemefinal String scheme = mPackageURI.getScheme();// 拒绝 file 和 package 之外的 schemeif (scheme != null && !"file".equals(scheme) && !"package".equals(scheme)) { // 记录不受支持的 scheme Log.w(TAG, "Unsupported scheme " + scheme); // 写入无效 URI 的安装结果 setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI); // 关闭当前界面 finish(); // 结束 onCreate 的后续处理 return;// 结束 scheme 检查}这次检查还不是跨进程安装调用。它先把明显不受支持的输入截住,并为确认页准备展示所需的数据。用户按下安装按钮后,InstallAppProgress 才承担进度显示和结果呈现。
file 与 package 两条路径也代表两类不同动作。file 指向一个需要复制和解析的安装包;package 指向系统已经知道的包名,可能用于重新启用或为当前用户安装已有包。后文讨论的 staging 目录、base.apk 复制和 PackageParser.parsePackage 都针对文件 URI 分支。

二、发起安装:触发跨进程调用
2.1 InstallAppProgress 传递的参数
文件 URI 分支会构造 PackageInstallObserver,再把 URI、installFlags、安装来源包名和校验参数交给 PackageManager。package scheme 代表安装系统中已有的包,走 installExistingPackage;本文的普通 APK 文件走 installPackageWithVerificationAndEncryption。
// 创建接收安装结果的观察者PackageInstallObserver observer = new PackageInstallObserver();// 判断是否要求启用系统中已有的包if ("package".equals(mPackageURI.getScheme())) { // 捕获已有包不存在的异常 try { // 尝试安装已有包 pm.installExistingPackage(mAppInfo.packageName); // 直接把成功结果交给观察者 observer.packageInstalled(mAppInfo.packageName, // 传入安装成功状态码 PackageManager.INSTALL_SUCCEEDED); // 处理系统中找不到该包的情况 } catch (PackageManager.NameNotFoundException e) { // 把无效 APK 结果交给观察者 observer.packageInstalled(mAppInfo.packageName, // 传入无效 APK 状态码 PackageManager.INSTALL_FAILED_INVALID_APK); // 结束异常处理 }// 文件 URI 进入完整安装流程} else { // 提交 APK URI、观察者、标志位、来源包名和校验参数 pm.installPackageWithVerificationAndEncryption(mPackageURI, observer, installFlags, // 传入安装来源、校验参数和空加密参数 installerPackageName, verificationParams, null);// 结束 URI 类型分支}2.2 observer 为什么是 Binder 服务端
安装工作发生在 system_server 中,安装界面运行在 PackageInstaller 应用进程。PMS 不能直接调用另一个进程里的普通 Java 对象,所以 PackageInstallObserver 继承 IPackageInstallObserver.Stub。Stub 是回调 Binder 的服务端;PMS 持有它的远程引用,完成安装后把状态码送回 PackageInstaller 进程。界面侧再把回调转换为 INSTALL_COMPLETE 消息,交给主线程更新页面。
// 定义安装结果回调的 Binder 服务端class PackageInstallObserver extends IPackageInstallObserver.Stub { // 接收包名与安装状态码 public void packageInstalled(String packageName, int returnCode) { // 创建安装完成消息 Message msg = mHandler.obtainMessage(INSTALL_COMPLETE); // 把状态码放入消息 msg.arg1 = returnCode; // 发送消息给界面线程 mHandler.sendMessage(msg); // 结束回调方法 }// 结束观察者类}这条继承关系最终到达 Binder。在本文限定的 Android 6.0 源码中,Binder 构造函数调用 native init();更新的版本改用 getNativeBBinderHolder 取得 native 侧对象,这份 6.0 源码里没有这个调用。
// 定义 Android 6.0 的 Binder 构造函数public Binder() { // 初始化 Binder 的 native 状态 init();// 结束构造函数}observer 的存活时间也说明了回调设计的必要性。安装请求返回并不表示 APK 已经安装完成,PackageInstaller 进程需要保留一个能被 PMS 调用的远程端点。若界面进程提前退出,PMS 捕获 RemoteException,系统侧安装结果仍然成立,只是原界面无法继续显示结果。回调负责通知,不参与决定安装是否成功。
2.3 ApplicationPackageManager 转入 IPackageManager
ApplicationPackageManager.installCommon 再检查一次 scheme,并取出文件路径。随后调用 mPM.installPackage;mPM 是 IPackageManager 接口,调用经 Binder 代理进入 system_server 中的 PackageManagerService。
// 定义旧安装接口共用的内部实现private void installCommon(Uri packageURI, // 接收观察者、标志位和安装来源包名 PackageInstallObserver observer, int flags, String installerPackageName, // 接收校验参数和容器加密参数 VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) { // 旧接口仅处理 file URI if (!"file".equals(packageURI.getScheme())) { // 对其他 scheme 抛出异常 throw new UnsupportedOperationException("Only file:// URIs are supported"); // 结束 scheme 检查 } // 拒绝携带容器加密参数的调用 if (encryptionParams != null) { // 旧接口不支持容器加密 throw new UnsupportedOperationException("ContainerEncryptionParams not supported"); // 结束加密参数检查 } // 取得 APK 的文件系统路径 final String originPath = packageURI.getPath(); // 捕获远程调用异常 try { // 通过 IPackageManager 代理提交安装请求 mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName, // 继续传入校验参数和空的 ABI 覆盖值 verificationParams, null); // 忽略远程端异常 } catch (RemoteException ignored) { // 结束异常处理 }// 结束 installCommon}三、PMS 收到请求:绑定 DefaultContainerService
3.1 请求先进入消息队列
PackageManagerService.installPackageAsUser 先校验 INSTALL_PACKAGES 权限和跨用户权限,也会检查 DISALLOW_INSTALL_APPS 限制。通过这些检查后,源码把来源路径、observer、标志位等数据封装成 InstallParams,再发送 INIT_COPY。安装请求由此进入 PackageHandler,调用线程不用同步等待复制和解析全部完成。
权限检查位于服务端,不能依赖 PackageInstaller 界面已经做过的判断。Binder 接口可能还有 shell、系统组件等调用方,PMS 必须按实际 calling UID 和目标 userId 统一裁决。INSTALL_FROM_ADB、INSTALL_ALL_USERS 等标志也会依据调用身份处理,客户端传入的整数并不会原封不动地成为最终策略。
// 定义按用户安装包的服务端入口public void installPackageAsUser(String originPath, IPackageInstallObserver2 observer, // 接收安装标志、来源包名和校验参数 int installFlags, String installerPackageName, VerificationParams verificationParams, // 接收 ABI 覆盖值和目标用户编号 String packageAbiOverride, int userId) { // 强制调用方持有安装权限 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, null); // 取得 Binder 调用方 UID final int callingUid = Binder.getCallingUid(); // 校验跨用户调用权限 enforceCrossUserPermission(callingUid, userId, true, true, "installPackageAsUser"); // 检查目标用户是否禁止安装应用 if (isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) { // 捕获回调远程异常 try { // 仅在观察者存在时回报失败 if (observer != null) { // 回报用户策略限制错误 observer.onPackageInstalled("", INSTALL_FAILED_USER_RESTRICTED, null, null); // 结束观察者判空 } // 忽略客户端回调异常 } catch (RemoteException re) { // 结束回调异常处理 } // 结束当前安装请求 return; // 结束用户限制检查 }// 判断调用方是否来自 shell 或 rootif ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) { // 标记安装来源为 ADB installFlags |= PackageManager.INSTALL_FROM_ADB;// 调用方持有安装权限时走另一分支} else { // 清除调用方传入的 ADB 来源标记 installFlags &= ~PackageManager.INSTALL_FROM_ADB; // 清除调用方传入的全部用户安装标记 installFlags &= ~PackageManager.INSTALL_ALL_USERS;// 结束调用身份分支}// 构造复制初始化消息final Message msg = mHandler.obtainMessage(INIT_COPY);// 把本次安装请求封装成 InstallParamsmsg.obj = new InstallParams(origin, null, observer, installFlags, installerPackageName, // 继续传入校验参数、目标用户和 ABI 覆盖值 null, verificationParams, user, packageAbiOverride, null);// 把消息交给 PackageHandlermHandler.sendMessage(msg);3.2 mBound 与 mPendingInstalls
PMS 不能收到 INIT_COPY 就直接复制。实际搬运字节的 DefaultContainerService 位于 com.android.defcontainer 进程,远程接口可用后才能调用。mBound 记录绑定状态,mPendingInstalls 保存等待处理的请求。
这里的队列解决了服务连接与安装请求到达时序不一致的问题。首个请求到来时,容器服务可能尚未启动;绑定属于异步操作,PMS 先保存请求。连接建立后,MCS_BOUND 再从队首取出参数执行。后续请求在服务已连接时仍进入同一队列,因此同一时刻只有队首请求会进入复制流程。
处理 INIT_COPY 时,未绑定就调用 connectToService();已经发起绑定则把请求加入队列。如果服务已连接且新请求位于队首,Handler 主动发送 MCS_BOUND,复用同一套队首处理逻辑。
// 定义连接容器服务的方法private boolean connectToService() { // 创建指向默认容器服务的显式 Intent Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); // 以 owner 用户绑定并按需创建服务 if (mContext.bindServiceAsUser(service, mDefContainerConn, // 指定自动创建标志与 owner 用户 Context.BIND_AUTO_CREATE, UserHandle.OWNER)) { // 记录已经发起绑定 mBound = true; // 返回绑定成功 return true; // 结束绑定成功分支 } // 返回绑定失败 return false;// 结束连接方法}// 处理复制初始化消息case INIT_COPY: { // 取出安装参数 HandlerParams params = (HandlerParams) msg.obj; // 记录请求在待安装队列中的位置 int idx = mPendingInstalls.size(); // 检查容器服务是否已经绑定 if (!mBound) { // 尝试连接默认容器服务 if (!connectToService()) { // 把服务错误传给当前请求 params.serviceError(); // 结束本次消息处理 return; // 处理绑定请求成功的情况 } else { // 把安装请求放入等待队列 mPendingInstalls.add(idx, params); // 结束绑定结果分支 } // 处理已经绑定的情况 } else { // 把安装请求放入等待队列 mPendingInstalls.add(idx, params); // 队首请求需要触发处理 if (idx == 0) { // 发送容器服务已绑定消息 mHandler.sendEmptyMessage(MCS_BOUND); // 结束队首检查 } // 结束绑定状态分支 } // 退出当前 case break;// 结束 INIT_COPY 分支}3.3 MCS_BOUND、失败与重试
服务连接回调把 IMediaContainerService 放进 MCS_BOUND 消息。Handler 保存远程接口,从 mPendingInstalls 取队首,并调用 startCopy()。该方法把远程异常转成 MCS_RECONNECT;调用次数超过 MAX_RETRIES(值为 4)后发送 MCS_GIVE_UP 并执行错误处理。绑定失败时,serviceError() 也会让请求沿失败路径收尾,不会把队列永久挂起。
MCS_BOUND 的队首处理位于 PackageManagerService.java L1170-L1192:
// 处理容器服务已经绑定的消息case MCS_BOUND: { // 按调试开关记录绑定消息 if (DEBUG_INSTALL) Slog.i(TAG, "mcs_bound"); // 检查消息是否携带远程服务 if (msg.obj != null) { // 保存容器服务 Binder 接口 mContainerService = (IMediaContainerService) msg.obj; // 结束远程服务判空 } // 处理容器服务接口为空的情况 if (mContainerService == null) { // 检查系统是否已经解除绑定 if (!mBound) { // 记录无法绑定容器服务 Slog.e(TAG, "Cannot bind to media container service"); // 遍历全部待安装请求 for (HandlerParams params : mPendingInstalls) { // 把绑定错误传给当前请求 params.serviceError(); // 结束待安装请求遍历 } // 清空无法继续处理的请求 mPendingInstalls.clear(); // 绑定仍在进行时进入等待分支 } else { // 记录正在等待容器服务 Slog.w(TAG, "Waiting to connect to media container service"); // 结束绑定状态判断 } // 服务可用且队列非空时处理队首 } else if (mPendingInstalls.size() > 0) { // 取得队首安装参数 HandlerParams params = mPendingInstalls.get(0); // 检查队首参数是否存在 if (params != null) { // 启动队首请求的复制阶段 if (params.startCopy()) { // 按调试开关记录后续队列检查 if (DEBUG_SD_INSTALL) Log.i(TAG, // 输出检查更多任务或解绑的日志 "Checking for more work or unbind..."); // 检查等待队列是否仍有任务 if (mPendingInstalls.size() > 0) { // 从等待队列删除已处理任务 mPendingInstalls.remove(0); // 结束队列非空检查 }上面的片段保留队首处理部分;原分支还包含远程服务为空时的报错与等待逻辑。
DefaultContainerConnection 的回调运行在 PMS 自己的 Binder 线程,因此 onServiceConnected 不直接开始复制,而是把远程接口随 MCS_BOUND 消息交给 Handler;真正的清理由 disconnectService() 完成,该方法会把 mContainerService 置空并解除绑定。
// 实现绑定 DefaultContainerService 的连接回调class DefaultContainerConnection implements ServiceConnection { // 服务连接成功时拿到远程接口 public void onServiceConnected(ComponentName name, IBinder service) { // 把 Binder 代理成 IMediaContainerService 接口 IMediaContainerService imcs = // 从 Binder 取得容器服务接口 IMediaContainerService.Stub.asInterface(service); // 把远程接口随 MCS_BOUND 消息发回 Handler 线程 mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs)); // 结束连接成功回调 } // 服务断开时只记日志,不在这里清空缓存 public void onServiceDisconnected(ComponentName name) { // 记录服务断开 if (DEBUG_SD_INSTALL) Log.i(TAG, "onServiceDisconnected"); // 结束连接断开回调 }// 结束连接回调类}startCopy() 的布尔返回值描述这一轮队列处理是否完成,并不等同于安装结果码。handleStartCopy() 会把复制结果保存在请求对象里,handleReturnCode() 再推进安装流程;远程异常时返回 false,Handler 保留重连机会。把“调度是否完成”和“包是否安装成功”拆开后,队列可以在容器服务短暂断开时恢复,而不是立即把所有等待请求判成坏包。
// 限制调用容器服务的重试次数private static final int MAX_RETRIES = 4;// 开始处理一次待安装请求final boolean startCopy() { // 保存本次处理结果 boolean res; // 捕获容器服务的远程异常 try { // 增加重试次数并检查上限 if (++mRetries > MAX_RETRIES) { // 通知 Handler 放弃连接 mHandler.sendEmptyMessage(MCS_GIVE_UP); // 执行服务错误处理 handleServiceError(); // 告知调用方本次未完成 return false; // 在重试范围内继续复制 } else { // 进入具体复制准备逻辑 handleStartCopy(); // 标记调用已经完成 res = true; // 结束重试判断 } // 捕获 Binder 远程异常 } catch (RemoteException e) { // 请求重新连接容器服务 mHandler.sendEmptyMessage(MCS_RECONNECT); // 标记本次未完成 res = false; // 结束异常处理 } // 处理复制阶段返回码 handleReturnCode(); // 返回本次处理状态 return res;// 结束 startCopy}
四、第一步:把 APK 复制到 /data/app
4.1 从 startCopy 到 copyApk
startCopy() 调用 InstallParams.handleStartCopy()。当安装校验没有阻止复制时,尾段执行 args.copyApk(mContainerService, true)。这里的 args 对普通内部存储安装是 FileInstallArgs,参数 true 表示使用临时 staging 目录。
createInstallArgs 根据移动安装、外部存储和 forward lock 等条件选择 InstallArgs 的具体实现,普通内部存储安装落到 FileInstallArgs。
// 根据安装参数选择具体的安装参数对象private InstallArgs createInstallArgs(InstallParams params) { // 移动安装走 MoveInstallArgs if (params.move != null) { // 返回移动安装参数 return new MoveInstallArgs(params); // 外部存储安装或 forward lock 走 ASEC 容器 } else if (installOnExternalAsec(params.installFlags) || params.isForwardLocked()) { // 返回 ASEC 安装参数 return new AsecInstallArgs(params); // 其余情况是内部存储的普通安装 } else { // 返回文件安装参数 return new FileInstallArgs(params); // 结束安装参数选择 }// 结束 createInstallArgs}调用 copyApk 前,handleStartCopy 先取得轻量包信息:真实设备上的“正在检查”阶段会据 recommendedInstallLocation 判断内部或外部存储,空间不足时先尝试释放缓存并重试;ret 已是非成功码时不会执行 copyApk。
// 向容器服务获取安装包的最小信息pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags, // 传入 ABI 覆盖值 packageAbiOverride);// 判断是否无需等待包校验} else { // 通过容器服务把 APK 复制到临时目录 ret = args.copyApk(mContainerService, true);// 结束校验状态分支}// 保存复制阶段返回码mRet = ret;4.2 临时目录与目标文件
FileInstallArgs.copyApk 先调用 PackageInstallerService.allocateStageDirLegacy(volumeUuid)。Android 6.0 的实现会在 /data/app 下分配 vmdl<sessionId>.tmp。这仍是临时目录;安装后看到的 /data/app/<包名>-<序号> 来自后面的重命名。
staging 目录让 PMS 可以在正式路径出现前完成复制、native 库处理和包校验。若中途失败,cleanUp() 删除这份临时内容;若成功,doRename 以目录重命名切换到正式代码路径。外部观察者不会把一个尚未完成扫描的目录误当作稳定安装结果,PMS 也能在失败分支集中回收文件。
PMS 随后创建 IParcelFileDescriptorFactory.Stub。容器服务并不自行决定 /data/app 中的落盘路径,而是把目标文件名交回 system_server,由 PMS 在 codeFile 下打开文件。Os.open 和 Os.chmod 都指定 0644。
// 定义文件安装参数的复制方法int copyApk(IMediaContainerService imcs, boolean temp) throws RemoteException { // 捕获临时目录分配失败 try { // 在目标卷分配旧式 staging 目录 final File tempDir = mInstallerService.allocateStageDirLegacy(volumeUuid); // 把代码目录指向 staging 目录 codeFile = tempDir; // 把资源目录指向同一 staging 目录 resourceFile = tempDir; // 处理目录创建异常 } catch (IOException e) { // 返回空间不足错误 return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; // 结束目录分配异常处理 } // 创建由 system_server 提供的目标文件工厂 final IParcelFileDescriptorFactory target = new IParcelFileDescriptorFactory.Stub() { // 按容器服务给出的名称打开目标文件 public ParcelFileDescriptor open(String name, int mode) throws RemoteException { // 拒绝非法文件名 if (!FileUtils.isValidExtFilename(name)) { // 抛出非法参数异常 throw new IllegalArgumentException("Invalid filename: " + name); // 结束文件名校验 } // 捕获底层文件系统调用异常 try { // 在 staging 目录下构造目标路径 final File file = new File(codeFile, name); // 以读写、创建和 0644 权限打开文件 final FileDescriptor fd = Os.open(file.getAbsolutePath(), // 指定打开标志与权限模式 O_RDWR | O_CREAT, 0644); // 再次把文件模式设为 0644 Os.chmod(file.getAbsolutePath(), 0644); // 把文件描述符交给远程调用方 return new ParcelFileDescriptor(fd); // 处理打开文件时的系统调用异常 } catch (ErrnoException e) { // 把系统调用异常转换成远程异常 throw new RemoteException("Failed to open: " + e.getMessage()); // 结束系统调用异常处理 } // 结束目标文件打开方法 } // 结束目标文件工厂 }; // 通过容器服务复制 APK ret = imcs.copyPackage(origin.file.getAbsolutePath(), target);4.3 DefaultContainerService 搬运字节
DefaultContainerService.copyPackage 先用 PackageParser.parsePackageLite 得到 PackageLite,用它识别 base APK 和 split APK。copyPackageInner 把基础包的目标名固定为 base.apk,split 包则写成 split_<splitName>.apk。copyFile 打开源文件,通过回调取得目标描述符,再用 Streams.copy 搬运内容。
// 定义容器服务的内部复制流程private int copyPackageInner(PackageLite pkg, IParcelFileDescriptorFactory target) // 声明文件与 Binder 调用可能抛出的异常 throws IOException, RemoteException { // 把基础 APK 复制为 base.apk copyFile(pkg.baseCodePath, target, "base.apk"); // 检查是否存在 split APK if (!ArrayUtils.isEmpty(pkg.splitNames)) { // 遍历全部 split APK for (int i = 0; i < pkg.splitNames.length; i++) { // 按 split 名称复制对应文件 copyFile(pkg.splitCodePaths[i], target, "split_" + pkg.splitNames[i] + ".apk"); // 结束 split 遍历 } // 结束 split 判空 } // 返回复制成功 return PackageManager.INSTALL_SUCCEEDED;// 结束内部复制流程}// 定义单文件复制方法private void copyFile(String sourcePath, IParcelFileDescriptorFactory target, String targetName) // 声明文件与 Binder 调用可能抛出的异常 throws IOException, RemoteException { // 声明输入流,初始为空 InputStream in = null; // 声明输出流,初始为空 OutputStream out = null; // 保证异常情况下也能释放流 try { // 打开源 APK 输入流 in = new FileInputStream(sourcePath); // 用 PMS 返回的文件描述符创建输出流 out = new ParcelFileDescriptor.AutoCloseOutputStream( // 让 PMS 按目标名称打开文件 target.open(targetName, ParcelFileDescriptor.MODE_READ_WRITE)); // 复制输入流中的字节 Streams.copy(in, out); // 无论成败都要释放流 } finally { // 安静地关闭输出流 IoUtils.closeQuietly(out); // 安静地关闭输入流 IoUtils.closeQuietly(in); // 结束资源释放 }// 结束单文件复制方法}这段实现解释了三个现象。基础包落盘后名为 base.apk,因为目标名由 copyPackageInner 固定传入;模式 0644 表示属主可读写、同组用户和其他用户只读,普通应用不拥有该文件及父目录的写权限,因此不能靠 File.delete() 删除它;复制量随 APK 体积增加,包体积会直接影响搬运阶段的耗时,解析和 native 库处理也会继续读取包内容。
0644 不能单独解释全部访问控制。文件属主、父目录权限和 SELinux 策略共同限制普通应用的操作;这里能从源码直接确认的是 PMS 以 0644 创建并修订目标文件模式。应用进程即使能读取自己的资源路径,也不因此获得修改 /data/app 中文件的权限。把“可读”推导成“可删除”会忽略目录写权限及强制访问控制。
容器服务使用 parsePackageLite 的目的也有限:它需要知道基础 APK 和 split APK 的组成,才能给每个文件选择目标名称。完整的组件、权限和应用属性解析发生在后面的 installPackageLI。因此,复制阶段出现一次轻量解析,并不表示第五节的完整包扫描可以省略。
临时目录安装成功后还要改名。doRename 调用 getNextCodePath(targetDir, pkg.packageName) 生成目标目录,并用 Os.rename 完成切换。Android 6.0 这里的后缀来自递增整数,不是 UUID。重命名后,代码同步更新 pkg.codePath、baseCodePath 以及 ApplicationInfo 中对应的路径。
// 取得 /data/app 下的父目录final File targetDir = codeFile.getParentFile();// 保存重命名前的 staging 目录final File beforeCodeFile = codeFile;// 按包名取得下一个正式代码目录final File afterCodeFile = getNextCodePath(targetDir, pkg.packageName);// 捕获目录重命名错误try { // 把 staging 目录改为正式目录名 Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath());// 处理底层文件系统错误} catch (ErrnoException e) { // 返回重命名失败 return false;// 结束重命名异常处理}
五、第二步:解析 AndroidManifest.xml 并写入 mPackages
5.1 processPendingInstall 的三段处理
复制阶段通过 handleReturnCode() 进入 processPendingInstall。PMS 把后续工作投递到 Handler:doPreInstall 处理安装前状态,installPackageLI 在 mInstallLock 保护下执行主体安装,doPostInstall 根据结果收尾。
// 定义待安装请求的处理入口private void processPendingInstall(final InstallArgs args, final int currentStatus) { // 把安装主体投递到 Handler mHandler.post(new Runnable() { // 定义异步任务内容 public void run() { // 创建安装结果对象 PackageInstalledInfo res = new PackageInstalledInfo(); // 继承复制阶段的返回码 res.returnCode = currentStatus; // 仅在前置阶段成功时继续安装 if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) { // 执行安装前处理 args.doPreInstall(res.returnCode); // 对主体安装过程加锁 synchronized (mInstallLock) { // 解析并安装包 installPackageLI(args, res); // 结束安装锁作用域 } // 执行安装后处理 args.doPostInstall(res.returnCode, res.uid); // 结束成功状态检查 }processPendingInstall 还会决定是否执行备份恢复:需要恢复时,PMS 把 token 交给 BackupManager 的 IBackupManager.restoreAtInstall,待备份管理器完成恢复后再回到 PMS 发送 POST_INSTALL;不需要恢复时直接发送 POST_INSTALL,所以该消息使用 token 查找 PostInstallData,而不是直接持有 InstallArgs。
// 判断本次是否为覆盖安装final boolean update = res.removedInfo.removedPackage != null;// 取得新应用的标志位,包解析失败时为 0final int flags = (res.pkg == null) ? 0 : res.pkg.applicationInfo.flags;// 非覆盖安装且应用允许备份时才准备恢复boolean doRestore = !update // 检查应用是否声明允许备份 && ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0);// 为这次安装后处理分配 tokenint token = mNextInstallToken++;// 把安装参数与结果登记到运行中的安装表mRunningInstalls.put(token, new PostInstallData(args, res));FileInstallArgs.doPreInstall 和 doPostInstall 在状态失败时调用 cleanUp()。doRename 也会在失败时清理临时目录。成功路径保留 staging 内容,待 installPackageLI 校验、扫描后将其改成正式目录。
// 定义安装前处理int doPreInstall(int status) { // 检查当前状态是否失败 if (status != PackageManager.INSTALL_SUCCEEDED) { // 清理临时安装内容 cleanUp(); // 结束状态检查 } // 返回当前安装状态 return status;// 结束安装前处理}// 定义安装后处理int doPostInstall(int status, int uid) { // 检查最终状态是否失败 if (status != PackageManager.INSTALL_SUCCEEDED) { // 清理残留安装内容 cleanUp(); // 结束状态检查 } // 返回最终安装状态 return status;// 结束安装后处理}三个方法的职责围绕同一个 PackageInstalledInfo.returnCode 展开。前置阶段接收复制结果,主体阶段可以把解析、签名或扫描错误写回结果对象,后置阶段再依据更新后的状态处理文件。mInstallLock 覆盖 installPackageLI,用于串行保护安装主体涉及的共享状态;耗时的远程复制已经在进入这把锁之前完成,避免把容器服务调用放在包管理安装锁内。
5.2 PackageParser 读取 APK
installPackageLI 取得 args.getCodePath() 指向的临时目录,组合 parseFlags,再调用 PackageParser.parsePackage。解析对象来自 APK 中的 AndroidManifest.xml 及相关包结构。解析异常会写入 PackageInstalledInfo 并终止这一轮安装。
// 定义锁内执行的安装主体private void installPackageLI(InstallArgs args, PackageInstalledInfo res) { // 取得 staging 目录 final File tmpPackageFile = new File(args.getCodePath()); // 取出本次安装的标志位 final int installFlags = args.installFlags; // 判断是否要求 forward lock 安装 final boolean forwardLocked = ((installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0); // 判断是否安装到外部存储 final boolean onExternal = (((installFlags & PackageManager.INSTALL_EXTERNAL) != 0) // 或者安装参数里已经指定了卷 UUID || (args.volumeUuid != null)); // 初始化扫描标志 int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE; // 组合解析标志 final int parseFlags = mDefParseFlags | PackageParser.PARSE_CHATTY // 按 forward lock 状态加入解析标志 | (forwardLocked ? PackageParser.PARSE_FORWARD_LOCK : 0) // 按外部存储状态加入解析标志 | (onExternal ? PackageParser.PARSE_EXTERNAL_STORAGE : 0); // 创建包解析器 PackageParser pp = new PackageParser(); // 声明解析后的包对象 final PackageParser.Package pkg; // 捕获包解析异常 try { // 解析 staging 目录中的 APK pkg = pp.parsePackage(tmpPackageFile, parseFlags); // 处理解析失败 } catch (PackageParserException e) { // 把错误写入安装结果 res.setError("Failed parse during installPackageLI", e); // 结束主体安装 return; // 结束解析异常处理 }后续逻辑根据 replace 选择 replacePackageLI 或 installNewPackageLI。两条分支都会继续做签名、权限、组件和包设置等检查;本文关注的共同结果是,扫描成功的 PackageParser.Package 被登记到 PMS 的索引中。
覆盖安装需要处理旧包信息与新包信息的衔接,新安装则要防止包名冲突并建立新的设置项。两条路径不能仅凭目录是否存在判断,replace 的计算还受安装标志和已安装包状态影响。执行包没有提供这些分支的完整片段,本文不展开未核对的条件,只保留 replacePackageLI 与 installNewPackageLI 的分流结论。
5.3 mPackages 是运行期索引
mPackages 是以包名为键、以解析后的 PackageParser.Package 为值的 ArrayMap 内存容器。
// 以包名为键保存已解析包的内存索引final ArrayMap<String, PackageParser.Package> mPackages = // 创建实际的索引容器 new ArrayMap<String, PackageParser.Package>();扫描流程在 synchronized (mPackages) 内更新设置,并以应用包名为键写入 mPackages。以后 AMS 或其他系统服务查询 Activity、Service、Provider 等组件时,可以从 PMS 已建立的结构中检索,无需在每次查询时重新解析 APK 的 XML。
// 锁住包索引synchronized (mPackages) { // 把新包设置写入 Settings mSettings.insertPackageSettingLPw(pkgSetting, pkg); // 以应用包名为键登记解析结果 mPackages.put(pkg.applicationInfo.packageName, pkg);// 结束包索引锁作用域}这里要区分内存状态与持久化状态:mPackages.put 明确说明 Package 已进入 PMS 的内存索引,但它本身不等同于 packages.xml 的落盘语句。读取 /data/system/packages.xml 还需要 root 或调试系统权限。
mSettings.insertPackageSettingLPw 与 mPackages.put 也不是同一张表的重复写入。前者维护包设置,后者保存运行期可查询的解析对象。组件解析结果进入 mPackages 后,系统服务可以按包名找到对应 Package,再访问其中的 Activity、Service、Provider 和权限声明。磁盘上的 APK 是代码与资源载体,mPackages 是 PMS 当前进程内组织这些信息的入口。
六、第三步:把结果送回发起进程
6.1 POST_INSTALL 负责系统侧收尾
主体安装完成后,PMS 用 token 保存 PostInstallData,并由 POST_INSTALL 消息统一收尾。成功安装会发出 Intent.ACTION_PACKAGE_ADDED;覆盖安装还会涉及 PACKAGE_REPLACED 和发给目标应用的 MY_PACKAGE_REPLACED。广播面向系统中的包变更监听者,observer 回调则面向发起本次安装的客户端,两者用途不同。
// 处理安装后消息case POST_INSTALL: { // 按 token 取得安装后数据 PostInstallData data = mRunningInstalls.get(msg.arg1); // 删除已经消费的 token mRunningInstalls.delete(msg.arg1); // 检查安装数据是否仍然存在 if (data != null) { // 取得安装参数 InstallArgs args = data.args; // 取得安装结果 PackageInstalledInfo res = data.res; // 成功时进入广播处理 if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) { // 取得新包的包名 final String packageName = res.pkg.applicationInfo.packageName; // 发送移除信息关联的广播 res.removedInfo.sendBroadcast(false, true, false); // 结束成功状态分支 }// 向首次安装该包的用户发广播sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, // 传入包名、附加信息和首批用户列表 packageName, extras, null, null, firstUsers);// 判断本次是否为覆盖安装final boolean update = res.removedInfo.removedPackage != null;// 覆盖安装需要标记替换状态if (update) { // 在附加信息里标记正在替换 extras.putBoolean(Intent.EXTRA_REPLACING, true);// 结束替换标记}// 向升级用户再发一次新增广播sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, // 传入包名、附加信息和升级用户列表 packageName, extras, null, null, updateUsers);// 覆盖安装还需要发送替换广播if (update) { // 通知其他应用该包已被替换 sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED, // 传入包名、附加信息和升级用户列表 packageName, extras, null, null, updateUsers); // 通知被替换的应用自身 sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED, // 目标与包名互换,并传入升级用户列表 null, null, packageName, null, updateUsers);// 结束替换广播}6.2 observer 把 returnCode 送回界面
广播处理和安装后工作完成后,PMS 调用最初保存的 observer。参数包含包名、返回码、错误消息和附加信息。Binder 把调用送回 PackageInstaller 进程,第二节中的 PackageInstallObserver.packageInstalled 再发出 INSTALL_COMPLETE 消息。进度页由此切换为安装成功或失败结果。
广播和 observer 的接收范围不同。包变更广播允许 Launcher、权限管理等系统参与者刷新自己的状态;observer 只对应这次安装请求,用来让发起方得到精确返回码。客户端消失不会撤销已经完成的包登记,广播接收者也不负责替 PackageInstaller 更新进度页。两条通知路径在 POST_INSTALL 汇合,但没有相互替代关系。
// 捕获安装结果回调的远程异常try { // 生成返回给客户端的附加信息 Bundle extras = extrasForInstallResult(res); // 通过 Binder observer 回报安装结果 args.observer.onPackageInstalled(res.name, res.returnCode, // 继续传入错误消息与附加信息 res.returnMsg, extras);// 处理客户端进程已经消失的情况} catch (RemoteException e) { // 记录 observer 已不存在 Slog.i(TAG, "Observer no longer exists.");// 结束回调异常处理}
七、把四步连成一条链
讲者笔记将普通应用安装归纳为四步:启动 PackageInstaller 的 PackageInstallerActivity;点击安装后调用 PKMS 的 installPackage,把 APK 复制到 /data/app 并命名为 base.apk;解析 AndroidManifest.xml,把 Package 对象放进 PKMS 的 mPackages;将安装结果回调给 PackageInstaller 进程。
源码为这四步补上了进程边界和中间状态。入口应用先展示确认页,InstallAppProgress 经 ApplicationPackageManager 和 IPackageManager 发起 Binder 调用。PMS 把请求放入 INIT_COPY 队列,绑定 DefaultContainerService 后执行远程复制。复制成功才进入 doPreInstall、installPackageLI、doPostInstall;扫描结果登记进 mPackages,POST_INSTALL 再处理广播和 observer 回调。

这条链中有两次方向相反的 Binder 通信:安装请求从 PackageInstaller 进程进入 system_server,复制调用从 system_server 进入 com.android.defcontainer;安装结果又通过 observer 从 system_server 返回 PackageInstaller 进程。Handler 消息把耗时阶段拆开,mPendingInstalls 和 token 分别维持复制队列与安装后任务的对应关系。
目录状态也沿着链路变化。来源 APK 起初位于下载目录或其他可读位置;复制开始后,/data/app 出现 vmdl<id>.tmp;完整解析和扫描通过后,目录改为带包名和递增序号的正式路径。base.apk 名称在复制时已经确定,目录名则要等解析得到包名并完成安装判断后才能确定。文件布局与 PMS 内存索引在成功路径上共同推进,任一阶段失败都会携带相应 returnCode 进入收尾。
八、小结与动手验证
Android 6.0 的普通 APK 安装可以从四类状态观察:入口界面是否取得正确 URI,PMS 是否进入复制队列,/data/app 是否形成 staging 与正式目录,PMS 是否登记包并发回结果。以下命令适合在测试设备或模拟器上交叉检查:
adb shell pm install /data/local/tmp/example.apkadb shell ls -l /data/appadb shell dumpsys package com.exampleadb logcat | grep -E 'PackageManager|PackageInstaller|DefContainer'pm install 可以绕过图形确认页直接触发包管理安装路径,因此适合观察 PMS 日志,但它不复现 PackageInstallerActivity 的交互。ls -l /data/app 可核对正式目录、base.apk 和权限;不同设备的 SELinux、shell 权限与构建类型可能限制读取。dumpsys package 用来确认包、组件和安装路径是否已经进入 PMS 的可查询状态。
验证时可以把日志、目录和 dumpsys 结果按同一次安装对照。日志中的复制成功仅表示 staging 阶段已经结束;正式目录出现说明重命名已经发生;dumpsys package 能查到目标包,才表明 PMS 已建立可查询的包状态。若界面提示失败,应记录 returnCode,再检查临时目录是否已经清理,不能依据某一条日志判断整次安装完成。
访问 /data/app 的完整内容或 /data/system/packages.xml 通常需要 root、userdebug/eng 构建或相应调试权限。讲者素材口述了 getPackageResourcePath() 与 getCodePath() 的打印结果,但执行包没有给出完整打印语句,也没有源码片段直接支持两者返回值必然一致,这一点保留为待核实。
本文结论适用于 AOSP android-6.0.0_r1 的旧安装接口。分析 Android 7.0 及以后版本时,应从 PackageInstaller.Session、staging 和 doPackageStage 重新追踪,不能依赖本文的入口调用关系。
本文由 AgentWorkFlow 生成