ADB 安装流程
本文承接 Session 安装、InstallingSession 会话、安装前检查 和 安装时 dexopt。主题是开发者输入的 adb install 如何在设备侧变成 cmd package install,再通过 shell 命令、PackageInstaller session 和 PMS 安装管道完成。
本文不重复 PackageInstaller 的一般 API,也不展开 adb 主机端传输协议的所有实现。重点放在 Android 17 源码中可以直接核对的设备侧路径:命令分发、stdin/文件/split 分叉、shell caller 归一化、session 写入、commit 等待和最终输出。
1. 端到端主线
1.1 调用关系
adb 主机端和 adbd 负责把命令送到设备;system_server 内真正解析安装参数的是 PackageManagerShellCommand。从该点开始,ADB 安装与其他 session 安装共用 PackageInstaller/PMS 管道,但 caller UID 和若干 flags 已经体现 shell 来源。
1.2 三种输入形态
| 输入 | cmd package 处理 | stage 写入方式 |
|---|---|---|
| 单 APK 路径 | runInstall() | doWriteSplits() |
stdin (-) | runInstall() + -S | doWriteSplit() 从 stdin 读 |
| streaming/incremental | runInstall-streaming/install-incremental | doAddFiles() + DataLoader |
| 多 APK | install-create/write/commit 或 runInstall 多参数 | 多次写入 base/split |
2. 命令分发
2.1 onCommand
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@Override
public int onCommand(String cmd) {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
try {
switch (cmd) {
case "install":
return runInstall();
case "install-streaming":
return runStreamingInstall();
case "install-incremental":
return runIncrementalInstall();
case "install-create":
return runInstallCreate();
case "install-write":
return runInstallWrite();
case "install-commit":
return runInstallCommit();
case "install-abandon":
case "install-destroy":
return runInstallAbandon();
default:
return handleDefaultCommands(cmd);
}
} catch (RemoteException e) {
getOutPrintWriter().println("Remote exception: " + e);
return -1;
}
}cmd package 的单步 install 与分步 session 命令在同一个 ShellCommand 中分发。adb install 通常使用 install,而 adb install-multiple 等场景可以显式使用 create/write/commit 三步。
2.2 runInstall 家族
private int runInstall() throws RemoteException {
return doRunInstall(makeInstallParams(UNSUPPORTED_INSTALL_CMD_OPTS));
}
private int runStreamingInstall() throws RemoteException {
final InstallParams params = makeInstallParams(UNSUPPORTED_INSTALL_CMD_OPTS);
if (params.sessionParams.dataLoaderParams == null) {
params.sessionParams.setDataLoaderParams(
PackageManagerShellCommandDataLoader.getStreamingDataLoaderParams(this));
}
return doRunInstall(params);
}
private int runIncrementalInstall() throws RemoteException {
final InstallParams params = makeInstallParams(UNSUPPORTED_INSTALL_CMD_OPTS);
if (params.sessionParams.dataLoaderParams == null) {
params.sessionParams.setDataLoaderParams(
PackageManagerShellCommandDataLoader.getIncrementalDataLoaderParams(this));
}
return doRunInstall(params);
}三种命令最终都进入 doRunInstall();差别在于是否预先给 SessionParams 设置 DataLoader 参数。streaming/incremental 不是另一套 PMS 安装实现,而是同一 session 管道的输入加载分支。
3. doRunInstall
3.1 启动与用户检查
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
private int doRunInstall(final InstallParams params) throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
if (!SystemProperties.getBoolean("sys.boot_completed", false)) {
pw.println("Error: device is still booting.");
return 1;
}
final int requestUserId = params.userId;
if (requestUserId != UserHandle.USER_ALL
&& requestUserId != UserHandle.USER_CURRENT) {
UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
UserInfo userInfo = umi.getUserInfo(requestUserId);
if (userInfo == null) {
pw.println("Failure [user " + requestUserId + " doesn't exist]");
return 1;
}
}设备尚未 boot completed 或指定用户不存在时,命令在创建 session 前直接失败。此时不会产生 stage,也不会触发 PackageInstallerService。
3.2 输入约束
final boolean isStreaming = params.sessionParams.dataLoaderParams != null;
final boolean isApex =
(params.sessionParams.installFlags & PackageManager.INSTALL_APEX) != 0;
final boolean installArchived =
(params.sessionParams.installFlags & PackageManager.INSTALL_ARCHIVED) != 0;
final ArrayList<String> args = getRemainingArgs();
final boolean fromStdIn = args.isEmpty() || STDIN_PATH.equals(args.get(0));
final boolean hasSplits = args.size() > 1;
if (fromStdIn && params.sessionParams.sizeBytes == -1) {
pw.println("Error: must either specify a package size or an APK file");
return 1;
}
if (isApex && hasSplits) {
pw.println("Error: can't specify SPLIT(s) for APEX");
return 1;
}
if (installArchived && hasSplits) {
pw.println("Error: can't have SPLIT(s) for Archival install");
return 1;
}stdin 必须配合 -S 大小;APEX 和归档安装不接受多 split。命令层先拒绝这些组合,避免把无效输入送入 session。
3.3 单步事务外壳
final int sessionId = doCreateSession(params.sessionParams,
params.installerPackageName, params.userId);
boolean abandonSession = true;
try {
if (isStreaming) {
if (doAddFiles(sessionId, args, params.sessionParams.sizeBytes,
isApex, installArchived) != PackageInstaller.STATUS_SUCCESS) {
return 1;
}
} else if (doWriteSplits(sessionId, args, params.sessionParams.sizeBytes,
isApex) != PackageInstaller.STATUS_SUCCESS) {
return 1;
}
if (doCommitSession(sessionId, false /*logSuccess*/)
!= PackageInstaller.STATUS_SUCCESS) {
return 1;
}
abandonSession = false;
pw.println("Success");
return 0;
} finally {
if (abandonSession) {
try {
doAbandonSession(sessionId, false /*logSuccess*/);
} catch (Exception ignore) {
}
}
}单步命令用 abandonSession 保护 session:写入或 commit 命令返回失败时主动 abandon;commit 调用被接受后才停止自动 abandon。对 staged session,commit 返回后可能只是 ready,命令还可以按 timeout 轮询 ready 状态。
4. Session 与 Shell
4.1 doCreateSession
private int doCreateSession(SessionParams params, String installerPackageName, int userId)
throws RemoteException {
if (userId == UserHandle.USER_ALL) {
params.installFlags |= PackageManager.INSTALL_ALL_USERS;
}
final int translatedUserId =
translateUserId(userId, UserHandle.USER_SYSTEM, "doCreateSession");
return mInterface.getPackageInstaller().createSession(
params, installerPackageName, null /*installerAttributionTag*/,
translatedUserId);
}Shell 命令通过 IPackageInstallerService Binder 创建 session;USER_ALL 先转换为安装 flag,实际 Binder 调用使用 system user 作为服务侧目标。
4.2 Service 归一化
if (PackageManagerServiceUtils.isRootOrShell(callingUid)
|| PackageInstallerSession.isSystemDataLoaderInstallation(params)
|| PackageManagerServiceUtils.isAdoptedShell(callingUid, mContext)) {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
installerPackageName = SHELL_PACKAGE_NAME;
} else {
params.installFlags &= ~PackageManager.INSTALL_FROM_ADB;
params.installFlags &= ~PackageManager.INSTALL_ALL_USERS;
params.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
}进入 service 后,shell/root caller 被标记为 INSTALL_FROM_ADB,并使用 shell installer name。这个标志影响后续 test-only、downgrade、验证和日志策略;它不是 adb 主机端自行写入的字符串。
5. 写入 APK
5.1 stdin 与本地文件
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
private int doAddFiles(int sessionId, ArrayList<String> args, long sessionSizeBytes,
boolean isApex, boolean installArchived) throws RemoteException {
PackageInstaller.Session session = null;
try {
session = new PackageInstaller.Session(
mInterface.getPackageInstaller().openSession(sessionId));
if (args.isEmpty() || STDIN_PATH.equals(args.get(0))) {
final String name = "base" + RANDOM.nextInt()
+ "." + (isApex ? "apex" : "apk");
final long size = installArchived ? -1 : sessionSizeBytes;
session.addFile(LOCATION_DATA_APP, name, size,
Metadata.forStdIn(name).toByteArray(), null);
return 0;
}
for (String arg : args) {
if (arg.indexOf(':') != -1) {
if (processArgForStdin(arg, session) != 0) {
return 1;
}
} else {
// Local file path is handled by the regular write helper.
if (doWriteSplit(sessionId, arg, -1,
new File(arg).getName(), false) != 0) {
return 1;
}
}
}
return 0;
} finally {
IoUtils.closeQuietly(session);
}
}streaming 模式通过 addFile() 描述输入;普通本地路径最终仍写入 session stage。stdin 的 metadata 记录文件名和大小,供后续验证和 data loader 使用。
5.2 openInFile
private Pair<ParcelFileDescriptor, Long> openInFile(String inPath, long sizeBytes)
throws IOException {
final ParcelFileDescriptor fd;
if (STDIN_PATH.equals(inPath)) {
fd = ParcelFileDescriptor.dup(getInFileDescriptor());
} else if (inPath != null) {
fd = openFileForSystem(inPath, "r");
if (fd == null) {
return Pair.create(null, -1L);
}
sizeBytes = fd.getStatSize();
} else {
fd = ParcelFileDescriptor.dup(getInFileDescriptor());
}
if (sizeBytes <= 0) {
getErrPrintWriter().println("Error: must specify an APK size");
return Pair.create(fd, 1L);
}
return Pair.create(fd, sizeBytes);
}stdin 通过 shell command 的输入 FD 复制;本地路径由 system 打开并读取 stat size。大小缺失或非正数时,命令在 session.write() 前失败。
5.3 doWriteSplit
private int doWriteSplit(int sessionId, String inPath, long sizeBytes,
String splitName, boolean logSuccess) throws RemoteException {
PackageInstaller.Session session = null;
try {
session = new PackageInstaller.Session(
mInterface.getPackageInstaller().openSession(sessionId));
final Pair<ParcelFileDescriptor, Long> fdWithSize =
openInFile(inPath, sizeBytes);
if (fdWithSize.first == null) {
return fdWithSize.second.intValue();
}
final ParcelFileDescriptor fd = fdWithSize.first;
session.write(splitName, 0, fdWithSize.second, fd);
if (logSuccess) {
getOutPrintWriter().println("Success: streamed "
+ fdWithSize.second + " bytes");
}
return 0;
} catch (IOException e) {
getErrPrintWriter().println("Error: failed to write; " + e.getMessage());
return 1;
} finally {
IoUtils.closeQuietly(session);
}
}每个输入文件都通过 Binder session 的 write() 进入 stage。这里的 splitName 是 session 内文件名,不是最终 Manifest split name;完整 Manifest 解析会在 PMS 后续阶段重新确认 split 关系。
5.4 多 APK 文件名
private int doWriteSplits(int sessionId, ArrayList<String> splitPaths,
long sessionSizeBytes, boolean isApex) throws RemoteException {
final boolean multipleSplits = splitPaths.size() > 1;
for (String splitPath : splitPaths) {
String splitName = multipleSplits
? new File(splitPath).getName()
: "base." + (isApex ? "apex" : "apk");
if (doWriteSplit(sessionId, splitPath, sessionSizeBytes,
splitName, false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) {
return 1;
}
}
return 0;
}多个本地 APK 逐个写入同一个 session;单文件则统一命名为 base.apk/base.apex。真正的 base 与 split 识别依赖 APK Manifest 的 split 属性,而非只依赖命令行文件名。
6. Commit 与结果
6.1 doCommitSession
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
private int doCommitSession(int sessionId, boolean logSuccess)
throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
PackageInstaller.Session session = null;
try {
session = new PackageInstaller.Session(
mInterface.getPackageInstaller().openSession(sessionId));
final LocalIntentReceiver receiver = new LocalIntentReceiver();
session.commit(receiver.getIntentSender());
if (!session.isStaged()) {
final Intent result = receiver.getResult();
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
List<String> warnings = result.getStringArrayListExtra(
PackageInstaller.EXTRA_WARNINGS);
if (status == PackageInstaller.STATUS_SUCCESS) {
if (!ArrayUtils.isEmpty(warnings)) {
for (String warning : warnings) {
pw.println("Warning: " + warning);
}
status = PackageInstaller.STATUS_FAILURE;
pw.println("Completed with warning(s)");
} else if (logSuccess) {
pw.println("Success");
}
} else {
pw.println("Failure ["
+ result.getStringExtra(
PackageInstaller.EXTRA_STATUS_MESSAGE) + "]");
}
return status;
}
// Staged result is checked by the caller after commit.
if (logSuccess) {
pw.println("Success");
}
return PackageInstaller.STATUS_SUCCESS;
} finally {
IoUtils.closeQuietly(session);
}
}普通 session 等待 LocalIntentReceiver 的结果;warnings 会被命令当作 failure 输出以提醒开发者。staged session 不在这里等待最终安装结果,由调用方轮询 ready/failed。
6.2 Staged 等待
if (params.sessionParams.isStaged && params.stagedReadyTimeoutMs > 0) {
return doWaitForStagedSessionReady(sessionId,
params.stagedReadyTimeoutMs, pw);
}
pw.println("Success");
return 0;adb install --staged 的命令成功通常只代表 session 已提交并进入 staged 状态;若指定 timeout,命令会进一步等待 SessionInfo.isStagedSessionReady() 或失败。
7. PMS 交接
7.1 Session 交接
7.2 Flags 下游影响
INSTALL_FROM_ADB 会被 InstallPackageHelper 读取,用于 test-only、downgrade、verification 和日志等策略。命令层的 -r/-d/-t/-g/--staged 等选项最终都成为 SessionParams flags,再由 service 和 prepare 阶段分别解释。
8. 失败与清理
8.1 命令层失败
| 失败位置 | 清理动作 |
|---|---|
| boot/user/input 检查 | 尚无 session,无需清理 |
| createSession 失败 | service 不登记 session |
| write/addFile 失败 | doRunInstall finally abandon |
| commit 调用失败 | abandonSession 删除 stage |
| 普通安装结果失败 | session/PMS 负责 code path 和状态清理 |
| staged 恢复失败 | StagingManager/session 记录 failed |
8.2 abandon 保护
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
boolean abandonSession = true;
try {
// write and commit
abandonSession = false;
} finally {
if (abandonSession) {
try {
doAbandonSession(sessionId, false /*logSuccess*/);
} catch (Exception ignore) {
}
}
}命令进程异常退出或写入失败时,finally 尽力 abandon。abandon 是否能立即删 stage 还取决于 PackageInstallerSession 的 mStageDirInUse;安装已经开始时,删除会延后到安全时机。
8.3 Warning 语义
doCommitSession() 对普通 session 收到 warnings 时输出 warning 并返回 failure status,即使 PackageInstaller 内部 status 是 success。这是 shell 命令的用户体验策略,不代表 PMS commit 失败;诊断时要区分安装状态和命令退出码。
9. 测试与诊断
9.1 输入与断言
| 输入 | 断言 | 覆盖范围 |
|---|---|---|
| boot 未完成 | 命令返回 1,未创建 session | 前置检查 |
stdin 无 -S | 命令拒绝 | 输入大小 |
| APEX + 多文件 | 命令拒绝 | 类型约束 |
adb install app.apk | create/write/commit 顺序正确 | 单步路径 |
| 多 APK | 同一 session 多次 write | split 输入 |
install-create/write/commit | 分步命令共享 sessionId | 手动 session |
| write 失败 | finally abandon | 命令清理 |
| 普通 commit warning | 输出 warning,命令返回 failure | 状态/退出码分离 |
| staged commit | 不等待最终 applied | staged 边界 |
| incremental | 设置 DataLoader 参数 | 输入加载分支 |
| shell caller | INSTALL_FROM_ADB 被设置 | service 归一化 |
测试应同时观察 shell 输出、session 状态、stage 文件和 PMS 最终结果;只检查 adb 退出码会遗漏“安装成功但有 warning”或 staged 仍在等待重启的情况。
9.2 现场排查顺序
- 在 host 侧确认发送的是单步
install还是分步 create/write/commit。 - 在设备侧确认
cmd package是否通过onCommand()进入正确分支。 - 检查
doRunInstall()的输入模式、用户和 size 参数。 - 查看 PackageInstallerService 是否创建 session,flags 是否包含
INSTALL_FROM_ADB。 - 检查 session stage 中 base/split 文件是否写完整、是否已经 sealed。
- 普通 session 查看
LocalIntentReceiverstatus/warnings;staged session 查看 ready/failed。 - 已交给 PMS 后转查 InstallingSession、InstallRequest 和 InstallPackageHelper。
10. 源码路线
建议按以下顺序阅读:
PackageManagerShellCommand.onCommand():命令分发。runInstall()/doRunInstall():单步安装外壳和失败 abandon。doCreateSession():ShellCommand 到 Binder service。PackageInstallerService.createSessionInternal():shell caller 与 flags 归一化。doAddFiles()/doWriteSplit()/doWriteSplits():stdin、本地文件和多 APK。PackageInstallerSession.write()/commit():stage 写入与封存。doCommitSession():普通/staged 结果输出差异。InstallingSession与InstallPackageHelper:进入 PMS 安装管道。
11. 设计收束
ADB 安装的真实链路是:
adb client
-> adbd shell / cmd package
-> PackageManagerShellCommand
-> PackageInstallerService session
-> stage write + commit
-> InstallingSession / InstallRequest
-> InstallPackageHelper
-> PMS 状态更新与结果回调ADB 的特殊性主要在入口身份和输入传输:shell caller 被标记为 INSTALL_FROM_ADB,stdin/多 APK/streaming 由命令层选择不同写入方式;一旦 session commit 并交给 PMS,后续 prepare、scan、reconcile、dexopt 和 commit 与其他安装路径共享。理解命令退出码、session status、PMS return code 和 staged ready 的层次差异,才能正确分析“命令失败但包已安装”“命令成功但 staged 尚未应用”和“写入失败后 stage 未清理”等现象。
