夜雨聆风学习资料网

ARTICLE · 1250412

Android 7 静默安装/卸载实现与 Framework 修改记录

Android 7 静默安装/卸载实现与 Framework 修改记录

在 RK 平台 Android 7 项目中,因为客户需要支持 APK 静默安装/卸载,只能从 Framework 层下手做一套完整方案。

网上常见做法基本都是直接调用 pm 命令。实测下来:

  • • pm install :基本没有问题,可以绕过“未知来源”提示完成安装
  • • pm uninstall :在应用内调用时会抛异常

最终还是回到系统源代码,从 Framework 层分析调用链并做了小改动,实现了可用的静默安装 / 卸载能力。

先看系统自带的卸载行为:从桌面拖动应用到“卸载”区域,本质上最终会调用 PackageManager.deletePackage 来完成卸载:

frameworks/base/core/java/android/content/pm/PackageManager.java

    /**     * Attempts to delete a package. Since this may take a little while, the     * result will be posted back to the given observer. A deletion will fail if     * the calling context lacks the     * {@link android.Manifest.permission#DELETE_PACKAGES} permission, if the     * named package cannot be found, or if the named package is a system     * package.     *     * @param packageName The name of the package to delete     * @param observer An observer callback to get notified when the package     *            deletion is complete.     *            {@link android.content.pm.IPackageDeleteObserver#packageDeleted}     *            will be called when that happens. observer may be null to     *            indicate that no callback is desired.     * @hide     */    @SuppressWarnings("HiddenAbstractMethod")    @RequiresPermission(Manifest.permission.DELETE_PACKAGES)    @UnsupportedAppUsage    public abstract void deletePackage(@NonNull String packageName,            @Nullable IPackageDeleteObserver observer, @DeleteFlags int flags);

不过 deletePackage 是 @hide 的隐藏 API,虽然可以通过引入 framework.jar 反射调用,但工程侵入性较大、维护成本也高,因此还是优先选择基于 pm 命令的方式来做静默卸载。

原理

pm 命令最终是落到 Framework 层的 PackageInstallerService 这个类来执行的。

相关源码文件位置如下:

frameworks/base/services/core/java/com/android/server/pm/PackageInstallerService.java

    @Override    public void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,                IntentSender statusReceiver, int userId) {        final Computer snapshot = mPm.snapshotComputer();        final int callingUid = Binder.getCallingUid();        snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");        // Create by yeruilai 2025-11-01 20:46:06 Enable silent loading for System users        // if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) {        if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID) && (callingUid != Process.SYSTEM_UID)) {            mAppOps.checkPackage(callingUid, callerPackageName);        }        // Check whether the caller is device owner or affiliated profile owner, in which case we do        // it silently.        DevicePolicyManagerInternal dpmi =                LocalServices.getService(DevicePolicyManagerInternal.class);        final boolean canSilentlyInstallPackage =                dpmi != null && dpmi.canSilentlyInstallPackage(callerPackageName, callingUid);

uninstall 相关逻辑默认只允许 ROOT_UID 与 SHELL_UID 这两个用户执行,所以:

  • • 通过 ADB shell 执行 pm uninstall 可以正常卸载(shell 权限)
  • • 在普通应用里直接执行 pm uninstall 就会因权限校验失败而抛异常

如果业务 APK 本身是系统应用(Manifest 中指定 android.uid.system),那么只需要在对应逻辑里把 SYSTEM_UID 也加入白名单,即可允许系统应用通过 pm uninstall 执行卸载。

如果希望 普通应用 也能调用 pm uninstall,那就更“简单粗暴”——把下面这行校验直接注释掉即可:

mAppOps.checkPackage(callingUid, callerPackageName);

但这等于完全放开了调用方校验,风险较大,仅建议在强可控的定制系统或调试环境里使用,不推荐在量产环境这样修改。

封装实现:静默安装 / 卸载工具类

下面是对静默安装 / 卸载做的一层简单封装,方便在业务代码中统一调用与回调处理。

1. 安装 / 卸载回调接口

public interface ApkInstallListener {    void onSuccess(String packageName);    void onFail(Exception e);}

2. 静默安装方法(基于 pm install)

public synchronized static void installApk(Context context,                                           String apkPath,                                           ApkInstallListener installListener) {    try {        PackageInfo info = getApkInfo(context, apkPath);        String[] args = {                "pm", "install",                "-i", info.packageName,                "--user", "0",                "-r", apkPath        };        ProcessBuilder processBuilder = new ProcessBuilder(args);        Process process = null;        BufferedReader successResult = null;        BufferedReader errorResult = null;        StringBuilder successMsg = new StringBuilder();        StringBuilder errorMsg = new StringBuilder();        try {            process = processBuilder.start();            successResult = new BufferedReader(                    new InputStreamReader(process.getInputStream()));            errorResult = new BufferedReader(                    new InputStreamReader(process.getErrorStream()));            String line;            while ((line = successResult.readLine()) != null) {                successMsg.append(line);            }            while ((line = errorResult.readLine()) != null) {                errorMsg.append(line);            }            Log.i(TAG, "install success_msg " + successMsg);            Log.i(TAG, "install error_msg " + errorMsg);            if (successMsg.toString().contains("Success")) {                if (installListener != null) {                    installListener.onSuccess(info.packageName);                }            } else {                if (installListener != null) {                    installListener.onFail(                            new RuntimeException(errorMsg.toString()));                }            }        } catch (Exception e) {            Log.i(TAG, "install fail ", e);            if (installListener != null) {                installListener.onFail(e);            }        } finally {            try {                if (successResult != null) {                    successResult.close();                }            } catch (IOException ignored) {            }            try {                if (errorResult != null) {                    errorResult.close();                }            } catch (IOException ignored) {            }            try {                if (process != null) {                    process.destroy();                }            } catch (Exception ignored) {            }        }    } catch (Exception e) {        if (installListener != null) {            installListener.onFail(e);        }    }}

3. 获取 APK 信息的工具方法

/** * 获取 apk 包的信息:版本号,名称,图标等 * * @param absPath apk 包的绝对路径 * @param context Context */private static PackageInfo getApkInfo(Context context, String absPath) {    PackageInfo pkgInfo = null;    try {        PackageManager pm = context.getPackageManager();        pkgInfo = pm.getPackageArchiveInfo(absPath,                PackageManager.GET_ACTIVITIES);        if (pkgInfo != null) {            ApplicationInfo appInfo = pkgInfo.applicationInfo;            // 必须加这两句,否则 icon 获取不到 apk 自身的图标            appInfo.sourceDir = absPath;            appInfo.publicSourceDir = absPath;            String appName = pm.getApplicationLabel(appInfo).toString();            String packageName = appInfo.packageName;            String version = pkgInfo.versionName;            String pkgInfoStr = String.format(                    "PackageName:%s, Version:%s, AppName:%s",                    packageName, version, appName);            Log.i("YTUtils",                    String.format("PkgInfo: %s", pkgInfoStr));        }    } catch (Exception e) {        Log.e("YTUtils",                "apk 文件信息读取失败: " + absPath, e);    }    return pkgInfo;}

4. 静默卸载方法(基于 pm uninstall)

public synchronized static void uninstallApk(Context context,                                             String packageName,                                             ApkInstallListener installListener) {    try {        String[] args = {                "pm", "uninstall",                "-k",                "--user", "0",                packageName        };        ProcessBuilder processBuilder = new ProcessBuilder(args);        Process process = null;        BufferedReader successResult = null;        BufferedReader errorResult = null;        StringBuilder successMsg = new StringBuilder();        StringBuilder errorMsg = new StringBuilder();        try {            process = processBuilder.start();            successResult = new BufferedReader(                    new InputStreamReader(process.getInputStream()));            errorResult = new BufferedReader(                    new InputStreamReader(process.getErrorStream()));            String line;            while ((line = successResult.readLine()) != null) {                successMsg.append(line);            }            while ((line = errorResult.readLine()) != null) {                errorMsg.append(line);            }            Log.i(TAG, "uninstall success_msg " + successMsg);            Log.i(TAG, "uninstall error_msg " + errorMsg);            if (successMsg.toString().contains("Success")) {                if (installListener != null) {                    installListener.onSuccess(packageName);                }            } else {                if (installListener != null) {                    installListener.onFail(                            new RuntimeException(errorMsg.toString()));                }            }        } catch (Exception e) {            Log.i(TAG, "uninstall fail ", e);            if (installListener != null) {                installListener.onFail(e);            }        } finally {            try {                if (successResult != null) {                    successResult.close();                }            } catch (IOException ignored) {            }            try {                if (errorResult != null) {                    errorResult.close();                }            } catch (IOException ignored) {            }            try {                if (process != null) {                    process.destroy();                }            } catch (Exception ignored) {            }        }    } catch (Exception e) {        if (installListener != null) {            installListener.onFail(e);        }    }}

以上就是在 Android 7 上,通过少量 Framework 修改配合 pm 命令,实现静默安装 / 卸载的整体思路与代码封装。

相关学习资料