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()

这个方法才是重头戏。我把它画成了一张流程图,方便你理解:

deletePackageX() 核心流程 deletePackageX() 1. 停止进程 (killApplication) 2. 删除 APK 文件 3. 清理数据目录 4. 发送广播 (PACKAGE_REMOVED) 5. 回调 observer

你想想看,这五个步骤里,最容易被忽略的是第 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 和包名删除
⚠️ 注意:从 Android 10 开始,系统引入了 CE(凭据加密)和 DE(直接加密)存储。如果只删了 /data/data/ 而没清理 CE/DE 目录,重新安装应用后可能出现「残留数据」问题。我曾经排查过一个 bug,用户卸载微信后重装,发现聊天记录还在——就是因为 CE 存储没清干净。

10.5 广播通知:谁在监听?

卸载完成后,系统会发送 ACTION_PACKAGE_REMOVED 广播。这个广播是 粘性广播,也就是说,即使广播发送时接收器还没注册,之后注册也能收到。

广播携带的 Extra 信息:

  • EXTRA_UID:被卸载应用的 UID
  • EXTRA_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 配置文件所有者阻止
🔍 关键点:这个回调是在 Binder 线程池中执行的,不是主线程。如果你要在回调里更新 UI,记得切到主线程。我见过有同事直接在这个回调里弹 Toast,结果崩溃了——因为 Binder 线程不允许直接操作 UI。

10.7 避坑指南:卸载流程中的常见问题

做系统开发这些年,我踩过不少卸载相关的坑。挑几个典型的说说:

  1. 卸载后进程残留:如果应用有 android:persistent="true" 属性,卸载后进程不会立即杀死。系统会在下次重启时才清理。我曾经排查过一个「卸载后还在跑」的 bug,就是这个原因。
  2. 多用户卸载不干净:Android 多用户模式下,一个用户卸载应用,其他用户的数据还在。需要传 DELETE_ALL_USERS 标志才能彻底清除。
  3. SD 卡上的应用:如果应用安装在 SD 卡上,卸载时还要清理 /mnt/asec/ 目录下的加密容器。这个步骤容易失败,导致 SD 卡上留下垃圾文件。
  4. 广播接收顺序PACKAGE_REMOVED 广播的发送顺序是 先动态注册的接收器,后静态注册的接收器。如果你的应用依赖这个广播做清理工作,要注意时序问题。

嗯,说到广播顺序,我记得有一次做系统升级,发现卸载广播总是先发给系统应用,再发给第三方应用。后来查代码才发现,PackageManagerService 里确实对广播发送顺序做了排序——系统应用的接收器优先级更高。

卸载流程看似简单,但每个环节都有值得深挖的细节。从权限检查到数据清理,再到广播通知,系统设计得相当严谨。你想想看,如果某个环节出了问题,轻则留下垃圾文件,重则影响系统稳定性。

我个人习惯在调试卸载问题时,打开 logcat 过滤 PackageManager 标签,能看到每一步的详细日志。这个方法帮我定位过不少疑难杂症。


公众号:蓝海资料掘金营,微信 deep3321