10. 卸载流程:PackageManager.deletePackage() 的完整链路,数据清理与广播通知
卸载一个 App,听起来就是点一下「删除」按钮的事。但系统背后要做的事情,比你想象的多得多。
我个人习惯把卸载流程拆成三个阶段:权限检查 → 数据清理 → 广播通知。每个阶段都有坑,咱们一个一个说。
10.1 入口:deletePackage() 到底干了什么?
你调用 PackageManager.deletePackage(),其实它只是个壳。真正的逻辑在 PackageManagerService 里。
来看核心代码(简化版):
// frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
public void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) {
// 1. 权限检查
if (!checkDeletePackagePermission(packageName, callingUid)) {
throw new SecurityException("...");
}
// 2. 准备卸载参数
PackageDeleteParams params = new PackageDeleteParams(packageName, flags);
// 3. 执行卸载
deletePackageX(packageName, observer, params);
}
嗯,这里要注意:flags 参数很关键。比如 DELETE_ALL_USERS 表示卸载所有用户的版本,DELETE_KEEP_DATA 表示保留数据。我见过不少开发者传错 flags,导致卸载不干净。
10.2 权限检查:谁有资格卸载?
系统会检查调用者的 UID。只有三种情况能通过:
- 系统应用(比如 Settings)
- 拥有 INSTALL_PACKAGES 权限的应用(比如应用商店)
- 应用自己卸载自己(UID 匹配)
我曾经遇到一个 case:第三方应用商店卸载应用时,因为 targetSdkVersion 升级后权限模型变了,导致卸载失败。说白了,就是权限检查没通过。
10.3 卸载核心链路:deletePackageX()
这个方法才是重头戏。我把它画成了一张流程图,方便你理解:
你想想看,这五个步骤里,最容易被忽略的是第 1 步——停止进程。如果应用有前台 Service 或者正在播放音乐,系统会先发一个 ACTION_PACKAGE_RESTARTED 广播,等进程真正停止后才继续卸载。
10.4 数据清理:不只是删文件夹
很多人以为数据清理就是 rm -rf /data/data/包名。其实远不止这些。
系统会清理以下几类数据:
| 数据类型 | 存储位置 | 清理方式 |
|---|---|---|
| 应用私有数据 | /data/data/包名/ | 递归删除目录 |
| 应用代码缓存 | /data/dalvik-cache/ | 删除对应 .dex 文件 |
| SharedPreferences | /data/data/包名/shared_prefs/ | 随数据目录一起删除 |
| 数据库文件 | /data/data/包名/databases/ | 随数据目录一起删除 |
| Native 库 | /data/app/包名/lib/ | 删除符号链接 |
| CE/DE 存储 | /data/misc/ce/ 和 /data/misc/de/ | 按用户 ID 和包名删除 |
10.5 广播通知:谁在监听?
卸载完成后,系统会发送 ACTION_PACKAGE_REMOVED 广播。这个广播是 粘性广播,也就是说,即使广播发送时接收器还没注册,之后注册也能收到。
广播携带的 Extra 信息:
EXTRA_UID:被卸载应用的 UIDEXTRA_DATA_REMOVED:是否删除了数据EXTRA_REPLACING:是否是覆盖安装(升级时也会触发卸载流程)
这里有个细节:EXTRA_REPLACING 为 true 时,表示这是应用升级,不是真正的卸载。很多第三方统计 SDK 没判断这个 flag,导致每次应用升级都上报一次「卸载事件」——数据完全不准。
intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)。如果是 true,直接 return,别做任何数据上报。
10.6 回调机制:observer 是怎么工作的?
调用 deletePackage() 时传的 IPackageDeleteObserver 是一个 Binder 回调。卸载完成后,系统会调用它的 packageDeleted() 方法。
来看一个典型实现:
// 应用商店中的卸载回调
private final IPackageDeleteObserver.Stub mDeleteObserver = new IPackageDeleteObserver.Stub() {
@Override
public void packageDeleted(String packageName, int returnCode) throws RemoteException {
if (returnCode == PackageManager.DELETE_SUCCEEDED) {
// 卸载成功
Log.i("Uninstall", packageName + " 卸载成功");
} else {
// 卸载失败,returnCode 包含错误原因
Log.w("Uninstall", "卸载失败,错误码: " + returnCode);
}
}
};
returnCode 的可能值:
| 常量 | 值 | 含义 |
|---|---|---|
| DELETE_SUCCEEDED | 1 | 卸载成功 |
| DELETE_FAILED_INTERNAL_ERROR | -1 | 内部错误 |
| DELETE_FAILED_DEVICE_POLICY_MANAGER | -2 | 设备管理器禁止卸载 |
| DELETE_FAILED_USER_RESTRICTED | -3 | 用户限制(多用户场景) |
| DELETE_FAILED_OWNER_BLOCKED | -4 | 配置文件所有者阻止 |
10.7 避坑指南:卸载流程中的常见问题
做系统开发这些年,我踩过不少卸载相关的坑。挑几个典型的说说:
- 卸载后进程残留:如果应用有
android:persistent="true"属性,卸载后进程不会立即杀死。系统会在下次重启时才清理。我曾经排查过一个「卸载后还在跑」的 bug,就是这个原因。 - 多用户卸载不干净:Android 多用户模式下,一个用户卸载应用,其他用户的数据还在。需要传
DELETE_ALL_USERS标志才能彻底清除。 - SD 卡上的应用:如果应用安装在 SD 卡上,卸载时还要清理
/mnt/asec/目录下的加密容器。这个步骤容易失败,导致 SD 卡上留下垃圾文件。 - 广播接收顺序:
PACKAGE_REMOVED广播的发送顺序是 先动态注册的接收器,后静态注册的接收器。如果你的应用依赖这个广播做清理工作,要注意时序问题。
嗯,说到广播顺序,我记得有一次做系统升级,发现卸载广播总是先发给系统应用,再发给第三方应用。后来查代码才发现,PackageManagerService 里确实对广播发送顺序做了排序——系统应用的接收器优先级更高。
卸载流程看似简单,但每个环节都有值得深挖的细节。从权限检查到数据清理,再到广播通知,系统设计得相当严谨。你想想看,如果某个环节出了问题,轻则留下垃圾文件,重则影响系统稳定性。
我个人习惯在调试卸载问题时,打开 logcat 过滤 PackageManager 标签,能看到每一步的详细日志。这个方法帮我定位过不少疑难杂症。