故事背景:新包裹的配送旅程
想象一下,你(用户)在网上订购了一个新玩具(一个 APK 应用)。这个玩具需要送到你家(Android 设备)并安装好才能玩。负责整个配送和安装流程的核心物流中心是 PMS(PackageManagerService),它就像设备上的超级仓库管理员,管理着所有已安装应用的“包裹信息”。
但 PMS 直接接触用户不太方便(系统服务安全性考虑),所以它有一个前台接待处叫 PackageManager。用户(你的应用代码)或系统应用(如应用商店)会找 PackageManager 说:“我要安装这个包裹(APK)!”。
今天的故事主角是系统自带的“专业安装员”应用—— PackageInstaller。它负责引导你完成安装确认、处理文件等步骤,最终把“包裹”交给 PMS 签收入库。我们聚焦在安装员接到“配送单”后,如何准备开始工作的过程(初始化)。
核心角色介绍 (代码映射)
-
PackageManagerService (PMS): 真正的“仓库总管”。藏在系统深处 (
system_server进程),拥有最终决定权(安装、卸载、查询权限等)。代码路径:frameworks/base/services/core/java/com/android/server/pm/。 -
PackageManager: PMS 的“前台”。应用进程通过它 (
Context.getPackageManager()) 与 PMS 通信(IPC)。它本身是抽象类,实际干活的是 ApplicationPackageManager (frameworks/base/core/java/android/app/ApplicationPackageManager.java)。就像你打电话给客服前台,客服再联系仓库。 -
PackageInstaller (App): 系统内置的专业安装应用 (
packages/apps/PackageInstaller)。包含 Activities (界面) 和逻辑,负责处理用户的安装请求。是我们故事的主角。 -
Intent: “配送单”。包含指令 (
ACTION_VIEW) 和包裹地址 (APK 的Uri)。 -
Uri: “包裹地址”。可能是:
file:///sdcard/app.apk(Android 7.0 前常用,但风险高 - 直接暴露文件路径)。content://com.example.fileprovider/my_files/app.apk(Android 7.0+ 安全方式 - 通过FileProvider中转)。
-
InstallStart: PackageInstaller 应用的第一个“接待窗口”(Activity)。负责初步查看“配送单”类型。
-
InstallStaging: PackageInstaller 的“临时中转站”。专门处理
content://这种安全地址,把它转换成file://地址,方便后续步骤。 -
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)。它:- 申请一个空的新纸箱 (
mStagedFile = TemporaryFileManager...)。 - 启动一个搬运工 (
StagingAsyncTask)。 - 搬运工打开安全箱 (
openInputStream),把里面的 APK 数据一块一块地 (byte[] buffer) 搬进新纸箱 (FileOutputStream)。 - 搬完后,把新纸箱的地址 (
Uri.fromFile(mStagedFile)) 贴在一张新的配送单上。 - 把新配送单发给核心工作台 (
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)。
- 打开 APK 文件 (
-
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 的初始化工作圆满完成!
-
入口引导 (InstallStart): 正确地将配送单 (
Intent) 根据Uri类型 (file://vscontent://) 路由到下一站。 -
安全中转 (InstallStaging): 成功地将安全的
content://地址转换成了可直接操作的文件 (File),为后续解析做好准备。 -
核心工作台就绪 (PackageInstallerActivity):
- 成功解析了 APK 文件,提取了包名、版本、权限等核心信息 (
mPkgInfo,mAppSnippet)。 - 完成了来源安全检查(未知来源处理)。
- 构建并展示了安装确认界面,清晰地向用户展示了应用信息和所需权限。
- 万事俱备,只等用户点击“安装”按钮!
- 成功解析了 APK 文件,提取了包名、版本、权限等核心信息 (
技术要点总结
- 分层设计: PMS (核心服务) <- PackageManager/ApplicationPackageManager (应用接口) <- PackageInstaller App (UI & 流程管理)。职责清晰,安全隔离。
- 安全演进: Android 7.0 引入
FileProvider强制使用content://Uri,避免直接文件路径暴露,是重要安全加固。InstallStaging就是为此适配的关键环节。 - APK 解析基石:
PackageParser(parsePackage) 是读取 APKAndroidManifest.xml信息的底层核心。PackageInfo是标准化的信息容器。 - 权限展示:
AppSecurityPermissions负责将枯燥的权限列表转化成用户可理解的视图。 - 未知来源管理: 流程中多处校验
mAllowUnknownSources或调用handleUnknownSources(),体现了 Android 对安装来源控制的重视。管理员策略 (DevicePolicyManager) 可以覆盖用户设置。 - 初始化目标: PackageInstallerActivity 初始化的最终成果就是准备好
mPkgInfo,mAppSnippet等数据,并渲染出安装确认对话框,等待用户交互。
这就是一个 APK 文件从用户点击“安装”开始,到系统准备好安装确认界面的精彩旅程!下一章(安装过程)将会讲述用户点击“安装”后,PackageInstaller 如何与 PMS 合作,最终完成应用的安装入库。希望这个故事让你对 Android 包管理的初始化流程有了更深入、更生动的理解!