Android-Framework(十三)-普通应用的安装过程

文章内容

本文探讨的问题是:用户在文件管理器中点开一个 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",外部应用不能直接启动进度页,确认页会在用户同意后从应用内部转入该界面。

xml
<!-- 声明权限确认与安装入口所在的 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 结束。

java
// 取得启动当前界面的 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 分支。

从点开 APK 到发起安装的调用链

二、发起安装:触发跨进程调用

2.1 InstallAppProgress 传递的参数

文件 URI 分支会构造 PackageInstallObserver,再把 URI、installFlags、安装来源包名和校验参数交给 PackageManager。package scheme 代表安装系统中已有的包,走 installExistingPackage;本文的普通 APK 文件走 installPackageWithVerificationAndEncryption。

java
// 创建接收安装结果的观察者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 消息,交给主线程更新页面。

java
// 定义安装结果回调的 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 源码里没有这个调用。

java
// 定义 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。

java
// 定义旧安装接口共用的内部实现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 等标志也会依据调用身份处理,客户端传入的整数并不会原封不动地成为最终策略。

java
// 定义按用户安装包的服务端入口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;    // 结束用户限制检查    }
java
// 判断调用方是否来自 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;// 结束调用身份分支}
java
// 构造复制初始化消息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,复用同一套队首处理逻辑。

java
// 定义连接容器服务的方法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;// 结束连接方法}
java
// 处理复制初始化消息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:

java
// 处理容器服务已经绑定的消息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 置空并解除绑定。

java
// 实现绑定 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 保留重连机会。把“调度是否完成”和“包是否安装成功”拆开后,队列可以在容器服务短暂断开时恢复,而不是立即把所有等待请求判成坏包。

java
// 限制调用容器服务的重试次数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}

PackageManagerService 内部的消息与绑定状态机

四、第一步:把 APK 复制到 /data/app

4.1 从 startCopy 到 copyApk

startCopy() 调用 InstallParams.handleStartCopy()。当安装校验没有阻止复制时,尾段执行 args.copyApk(mContainerService, true)。这里的 args 对普通内部存储安装是 FileInstallArgs,参数 true 表示使用临时 staging 目录。

createInstallArgs 根据移动安装、外部存储和 forward lock 等条件选择 InstallArgs 的具体实现,普通内部存储安装落到 FileInstallArgs。

java
// 根据安装参数选择具体的安装参数对象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。

java
// 向容器服务获取安装包的最小信息pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags,        // 传入 ABI 覆盖值        packageAbiOverride);
java
// 判断是否无需等待包校验} 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。

java
// 定义文件安装参数的复制方法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 搬运内容。

java
// 定义容器服务的内部复制流程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 中对应的路径。

java
// 取得 /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;// 结束重命名异常处理}

临时目录与 base.apk 的 0644 权限位

五、第二步:解析 AndroidManifest.xml 并写入 mPackages

5.1 processPendingInstall 的三段处理

复制阶段通过 handleReturnCode() 进入 processPendingInstall。PMS 把后续工作投递到 Handler:doPreInstall 处理安装前状态,installPackageLI 在 mInstallLock 保护下执行主体安装,doPostInstall 根据结果收尾。

java
// 定义待安装请求的处理入口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。

java
// 判断本次是否为覆盖安装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 校验、扫描后将其改成正式目录。

java
// 定义安装前处理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 并终止这一轮安装。

java
// 定义锁内执行的安装主体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 内存容器。

java
// 以包名为键保存已解析包的内存索引final ArrayMap<String, PackageParser.Package> mPackages =        // 创建实际的索引容器        new ArrayMap<String, PackageParser.Package>();

扫描流程在 synchronized (mPackages) 内更新设置,并以应用包名为键写入 mPackages。以后 AMS 或其他系统服务查询 Activity、Service、Provider 等组件时,可以从 PMS 已建立的结构中检索,无需在每次查询时重新解析 APK 的 XML。

java
// 锁住包索引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 回调则面向发起本次安装的客户端,两者用途不同。

java
// 处理安装后消息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);        // 结束成功状态分支        }
java
// 向首次安装该包的用户发广播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 汇合,但没有相互替代关系。

java
// 捕获安装结果回调的远程异常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.");// 结束回调异常处理}

安装三段式与 POST_INSTALL 收尾

七、把四步连成一条链

讲者笔记将普通应用安装归纳为四步:启动 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 是否登记包并发回结果。以下命令适合在测试设备或模拟器上交叉检查:

bash
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 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android-Framework(十三)-普通应用的安装过程 · Yinchen 的博客