系统自带的“专业安装员”应用—— ​​PackageInstaller

141 阅读10分钟

故事背景:新包裹的配送旅程​

想象一下,你(用户)在网上订购了一个新玩具(一个 APK 应用)。这个玩具需要送到你家(Android 设备)并安装好才能玩。负责整个配送和安装流程的核心物流中心是 ​​PMS(PackageManagerService)​​,它就像设备上的超级仓库管理员,管理着所有已安装应用的“包裹信息”。

但 PMS 直接接触用户不太方便(系统服务安全性考虑),所以它有一个前台接待处叫 ​​PackageManager​​。用户(你的应用代码)或系统应用(如应用商店)会找 PackageManager 说:“我要安装这个包裹(APK)!”。

今天的故事主角是系统自带的“专业安装员”应用—— ​​PackageInstaller​​。它负责引导你完成安装确认、处理文件等步骤,最终把“包裹”交给 PMS 签收入库。我们聚焦在安装员接到“配送单”后,如何准备开始工作的过程(初始化)。


​核心角色介绍 (代码映射)​

  1. ​PackageManagerService (PMS):​​ 真正的“仓库总管”。藏在系统深处 (system_server进程),拥有最终决定权(安装、卸载、查询权限等)。代码路径:frameworks/base/services/core/java/com/android/server/pm/

  2. ​PackageManager:​​ PMS 的“前台”。应用进程通过它 (Context.getPackageManager()) 与 PMS 通信(IPC)。它本身是抽象类,实际干活的是 ​​ApplicationPackageManager​​ (frameworks/base/core/java/android/app/ApplicationPackageManager.java)。就像你打电话给客服前台,客服再联系仓库。

  3. ​PackageInstaller (App):​​ 系统内置的专业安装应用 (packages/apps/PackageInstaller)。包含 Activities (界面) 和逻辑,负责处理用户的安装请求。是我们故事的主角。

  4. ​Intent:​​ “配送单”。包含指令 (ACTION_VIEW) 和包裹地址 (APK 的 Uri)。

  5. ​Uri:​​ “包裹地址”。可能是:

    • file:///sdcard/app.apk (Android 7.0 前常用,但风险高 - 直接暴露文件路径)。
    • content://com.example.fileprovider/my_files/app.apk (Android 7.0+ 安全方式 - 通过 FileProvider 中转)。
  6. ​InstallStart:​​ PackageInstaller 应用的第一个“接待窗口”(Activity)。负责初步查看“配送单”类型。

  7. ​InstallStaging:​​ PackageInstaller 的“临时中转站”。专门处理 content:// 这种安全地址,把它转换成 file:// 地址,方便后续步骤。

  8. ​PackageInstallerActivity:​​ PackageInstaller 的核心“工作台”(Activity)。在这里,安装员会打开包裹检查(解析 APK)、让你确认是否安装、处理未知来源问题等。


​故事展开:配送单的旅程 (代码流程)​

​第1步:用户下单 (启动安装)​

  • ​用户操作:​​ 在文件管理器点击 APK,或在你的 App 中调用安装代码。

  • ​代码示例 (通用):​

    java
    Copy
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
    
  • ​关键点:​​ intent.setDataAndType(...) 设置了包裹地址 (apkUri) 和明确的 MIME 类型 (application/vnd...),告诉系统“这是个要安装的 APK 文件”。

​第2步:接待处初步分拣 (InstallStart)​

  • ​系统行为:​​ Android 系统根据 Intent 的 Action 和 Type,找到能处理它的 Activity。PackageInstaller 的 AndroidManifest.xml 声明了 InstallStart 能处理这类配送单:

    xml
    Copy
    <activity android:name=".InstallStart" ...>
        <intent-filter ...>
            <action android:name="android.intent.action.VIEW" />
            <action android:name="android.intent.action.INSTALL_PACKAGE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:scheme="file" />
            <data android:scheme="content" />
            <data android:mimeType="application/vnd.android.package-archive" />
        </intent-filter>
    </activity>
    
  • ​InstallStart 的工作 (onCreate):​

    java
    Copy
    // packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        Uri packageUri = getIntent().getData();
        if (packageUri == null) { ... } // 错误:没地址!
        if (packageUri.getScheme().equals(SCHEME_CONTENT)) { // 是content://地址?
            nextActivity.setClass(this, InstallStaging.class); // 送中转站
        } else {
            nextActivity.setClass(this, PackageInstallerActivity.class); // 直接送工作台
        }
        startActivity(nextActivity);
        finish(); // 我的任务完成了
    }
    
  • ​故事比喻:​​ 前台(InstallStart)拿到配送单,一看地址:

    • 是 content:// (加密安全箱)? -> 交给专门的中转站(InstallStaging)处理。
    • 是 file:// (普通快递盒) 或其它? -> 直接送到工作台(PackageInstallerActivity)开箱检查。
    • 没地址?-> 直接报错拒收!

​第3步:中转站拆安全箱 (InstallStaging - 仅限 content://)​

  • ​为什么需要中转?​​ Android 7.0+ 为了安全,禁止 App 间直接传递 file:// 路径(防止恶意 App 偷窥文件)。FileProvider 提供了 content:// 这种安全的共享方式。但 PackageInstaller 的核心工作台 (PackageInstallerActivity) 最终需要直接操作文件 (File 对象),所以需要先把 content:// 里的内容“倒腾”出来,保存成一个真正的临时文件 (File)。

  • ​InstallStaging 的工作 (onResume & StagingAsyncTask):​

    java
    Copy
    // packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
    @Override
    protected void onResume() {
        super.onResume();
        mStagedFile = TemporaryFileManager.getStagedFile(this); // 1. 申请一个临时空盒子
        mStagingTask = new StagingAsyncTask();
        mStagingTask.execute(getIntent().getData()); // 2. 启动任务:把安全箱内容倒进空盒子
    }
    
    private class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> {
        @Override
        protected Boolean doInBackground(Uri... params) {
            Uri packageUri = params[0];
            try (InputStream in = getContentResolver().openInputStream(packageUri)) { // 打开安全箱
                try (OutputStream out = new FileOutputStream(mStagedFile)) { // 打开临时盒
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) >= 0) { // 一桶一桶倒数据
                        out.write(buffer, 0, bytesRead);
                    }
                }
            } catch (...) { ... return false; }
            return true;
        }
    
        @Override
        protected void onPostExecute(Boolean success) {
            if (success) {
                Intent newIntent = new Intent(getIntent());
                newIntent.setClass(InstallStaging.this, PackageInstallerActivity.class);
                newIntent.setData(Uri.fromFile(mStagedFile)); // 关键!把临时文件地址放进新配送单
                startActivity(newIntent); // 送到工作台!
            } else { ... }
        }
    }
    
  • ​故事比喻:​​ 中转站(InstallStaging)收到装有 APK 的安全箱 (content:// Uri)。它:

    1. 申请一个空的新纸箱 (mStagedFile = TemporaryFileManager...)。
    2. 启动一个搬运工 (StagingAsyncTask)。
    3. 搬运工打开安全箱 (openInputStream),把里面的 APK 数据一块一块地 (byte[] buffer) 搬进新纸箱 (FileOutputStream)。
    4. 搬完后,把新纸箱的地址 (Uri.fromFile(mStagedFile)) 贴在一张新的配送单上。
    5. 把新配送单发给核心工作台 (PackageInstallerActivity)。

​第4步:抵达核心工作台 - 拆箱验货 (PackageInstallerActivity)​

  • ​终于来到主角舞台!​​ PackageInstallerActivity 是安装流程的核心界面和管理者。它要做:

    • 打开包裹(解析 APK 文件)。
    • 检查包裹是否完整(包信息)。
    • 询问你是否信任这个来源(未知来源处理)。
    • 展示包裹内容(应用图标、名称、需要的权限)。
    • 等你点击“安装”按钮。
  • ​初始化工作 (onCreate):​

    java
    Copy
    // packages/apps/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
    @Override
    protected void onCreate(Bundle icicle) {
        // ... 获取各种“工具”:仓库前台(PackageManager), 仓库总机(IPackageManager),
        //    权限检查员(AppOpsManager), 安装小助手(PackageInstaller), 用户管理员(UserManager)
        mPm = getPackageManager();
        mIpm = AppGlobals.getPackageManager(); // 获取系统级PackageManager (IPackageManager)
        mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
        mInstaller = mPm.getPackageInstaller(); // PackageInstaller对象
        mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
    
        // 关键步骤1: 处理包裹地址(Uri),解析APK!
        boolean wasSetUp = processPackageUri(packageUri);
        if (!wasSetUp) return; // 解析失败就收工
    
        // 关键步骤2: 检查未知来源 & 启动安装确认流程
        checkIfAllowedAndInitiateInstall();
    }
    
  • ​核心子步骤 4.1: 拆包验货 (processPackageUri)​

    java
    Copy
    private boolean processPackageUri(final Uri packageUri) {
        mPackageURI = packageUri;
        final String scheme = packageUri.getScheme();
    
        switch (scheme) {
            case SCHEME_FILE: { // 最常见的情况(来自InstallStaging中转或旧方式)
                File sourceFile = new File(packageUri.getPath()); // 1. 根据路径创建File对象
                // 2. 核心解析!获取初步包裹信息(PackageParser.Package)
                PackageParser.Package parsed = PackageUtil.getPackageInfo(this, sourceFile);
                if (parsed == null) { ... return false; } // 包裹坏了!
                // 3. 生成更详细的包裹清单(PackageInfo)
                mPkgInfo = PackageParser.generatePackageInfo(parsed, ... );
                // 4. 获取应用图标和名称摘要 (AppSnippet)
                mAppSnippet = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
                break;
            }
            case SCHEME_PACKAGE: { ... } // 处理package:协议(更新等)
            default: { ... return false; } // 不认识地址,拒收!
        }
        return true; // 验货成功!
    }
    
    • ​代码详解 (PackageUtil.getPackageInfo 内部):​​ 这个函数底层会调用 PackageParser.parsePackage() (在 frameworks/base/core/java/android/content/pm/PackageParser.java)。想象一个经验丰富的验货员:

      • 打开 APK 文件 (sourceFile)。
      • 仔细阅读里面的 AndroidManifest.xml (相当于包裹里的说明书)。
      • 提取出包名 (packageName)、版本号 (versionCode)、所需权限 (<uses-permission>)、四大组件 (<activity><service>等) 等核心信息,封装成一个 PackageParser.Package 对象 (parsed)。
    • PackageParser.generatePackageInfo:​​ 基于 parsed 的基础信息,结合当前设备的用户状态、权限配置等,生成更标准化的 PackageInfo 对象 (mPkgInfo),这是 PMS 和 PackageManager 广泛使用的格式。

    • PackageUtil.getAppSnippet:​​ 从 APK 中提取出应用图标(icon)和名称(label),用于在安装界面向用户展示。这通常是通过解析 ApplicationInfo 里的 icon 和 label 资源ID,然后加载对应的资源得到的。

  • ​核心子步骤 4.2: 来源核查与启动确认 (checkIfAllowedAndInitiateInstall)​

    java
    Copy
    private void checkIfAllowedAndInitiateInstall() {
        // 情况1: 允许安装未知来源 或 这个包裹来源明确可信(比如系统更新、Play商店)
        if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) {
            initiateInstall(); // 直接走安装确认流程!
            return;
        }
        // 情况2: 设备管理员严格禁止未知来源安装
        if (isUnknownSourcesDisallowed()) {
            showDialogInner(DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER); // 弹出提示
            // 或者跳转到管理员设置界面 (startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS)))
            return;
        }
        // 情况3: 普通情况 - 需要询问用户是否允许“未知来源”
        handleUnknownSources(); // 弹出系统设置提示框或跳转到“未知来源”开关页面
    }
    
    private void initiateInstall() {
        // 1. 获取包名
        String pkgName = mPkgInfo.packageName;
        // ... (处理包名重定向等罕见情况)
    
        // 2. 尝试查询设备上是否已存在同名应用 (MATCH_UNINSTALLED_PACKAGES 包括已卸载但保留数据的)
        try {
            mAppInfo = mPm.getApplicationInfo(pkgName, PackageManager.MATCH_UNINSTALLED_PACKAGES);
            if ((mAppInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
                mAppInfo = null; // 标记为未安装(可能是更新残留数据)
            }
        } catch (NameNotFoundException e) {
            mAppInfo = null; // 完全没安装过
        }
    
        // 3. 启动安装确认界面 (展示权限等)
        startInstallConfirm();
    }
    
    • mAllowUnknownSources:​​ 这个标志位通常在之前(比如设置里)已经由用户设置过是否允许安装非应用商店来源的 APK。
    • isInstallRequestFromUnknownSource:​​ 系统根据 Intent 的来源(比如是否由 Verified 的应用商店发起)判断是否“未知”。
    • handleUnknownSources:​​ 如果需要用户授权未知来源,它会触发系统对话框引导用户去设置开启。
  • ​核心子步骤 4.3: 展示包裹详情并等待确认 (startInstallConfirm)​

    java
    Copy
    private void startInstallConfirm() {
        // ... 复杂的界面初始化代码 (设置标题、按钮、回调等)
        // 1. 创建权限展示器
        AppSecurityPermissions perms = new AppSecurityPermissions(this, mPkgInfo);
        // 2. 计算权限数量 (所有权限)
        final int N = perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
    
        // 如果是更新应用...
        if (mAppInfo != null) {
            // ... 设置更新提示文字 ...
            // 3. 检查是否有“新增”权限 (相比旧版本)
            boolean newPermissionsFound = false;
            if (!supportsRuntimePermissions) { // 针对旧系统 (Android < 6.0)
                newPermissionsFound = (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);
            }
            if (newPermissionsFound) {
                // 4. 将“新增权限”视图添加到滚动区域展示给用户
                mScrollView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_NEW));
            }
        }
        // ... (处理首次安装的权限展示逻辑)
        // 5. 显示应用图标、名称、版本号 (mAppSnippet)
        // 6. 显示确认(Install)和取消(Cancel)按钮
    }
    
    • AppSecurityPermissions:​​ 这个类专门负责从 PackageInfo 或 PackageParser.Package 中提取权限信息 (<uses-permission>),并将其组织成可视化的列表。
    • perms.getPermissionsView():​​ 生成一个 View (通常是 LinearLayout 包含多个 PermissionItemView),每个 PermissionItemView 展示一个权限组(如“位置”、“通讯录”)的图标和描述。用户看到的就是那个熟悉的权限列表。
    • ​WHICH_NEW/ALL:​​ 用于筛选展示哪些权限(所有权限 vs 本次更新新增的权限)。

​故事尾声:准备就绪​

至此,​​PackageInstaller 的初始化工作圆满完成!​

  1. ​入口引导 (InstallStart):​​ 正确地将配送单 (Intent) 根据 Uri 类型 (file:// vs content://) 路由到下一站。

  2. ​安全中转 (InstallStaging):​​ 成功地将安全的 content:// 地址转换成了可直接操作的文件 (File),为后续解析做好准备。

  3. ​核心工作台就绪 (PackageInstallerActivity):​

    • 成功解析了 APK 文件,提取了包名、版本、权限等核心信息 (mPkgInfomAppSnippet)。
    • 完成了来源安全检查(未知来源处理)。
    • 构建并展示了安装确认界面,清晰地向用户展示了应用信息和所需权限。
    • 万事俱备,只等用户点击“安装”按钮!

​技术要点总结​

  1. ​分层设计:​​ PMS (核心服务) <- PackageManager/ApplicationPackageManager (应用接口) <- PackageInstaller App (UI & 流程管理)。职责清晰,安全隔离。
  2. ​安全演进:​​ Android 7.0 引入 FileProvider 强制使用 content:// Uri,避免直接文件路径暴露,是重要安全加固。InstallStaging 就是为此适配的关键环节。
  3. ​APK 解析基石:​​ PackageParser (parsePackage) 是读取 APK AndroidManifest.xml 信息的底层核心。PackageInfo 是标准化的信息容器。
  4. ​权限展示:​​ AppSecurityPermissions 负责将枯燥的权限列表转化成用户可理解的视图。
  5. ​未知来源管理:​​ 流程中多处校验 mAllowUnknownSources 或调用 handleUnknownSources(),体现了 Android 对安装来源控制的重视。管理员策略 (DevicePolicyManager) 可以覆盖用户设置。
  6. ​初始化目标:​​ PackageInstallerActivity 初始化的最终成果就是准备好 mPkgInfomAppSnippet 等数据,并渲染出安装确认对话框,等待用户交互。

这就是一个 APK 文件从用户点击“安装”开始,到系统准备好安装确认界面的精彩旅程!下一章(安装过程)将会讲述用户点击“安装”后,PackageInstaller 如何与 PMS 合作,最终完成应用的安装入库。希望这个故事让你对 Android 包管理的初始化流程有了更深入、更生动的理解!