Android 应用内 APK 安装全方案:从静默安装到普通安装的详解
> 场景:TV 盒子、教育平板、企业 MDM 设备,需要在应用内完成 APK 升级或第三方应用推送安装。
> 已在 Android 4.4 ~ 12 的真机及模拟器上验证,可直接用于生产环境。
一、先搞清楚:你的应用能走哪条路?
| 安装方式 | 所需权限 | 用户感知 | 适用场景 |
|---|---|---|---|
| 静默安装 | INSTALL_PACKAGES(系统级) |
无弹窗,后台完成 | 系统签名应用、厂商预装、Root 设备 |
| 普通安装 | REQUEST_INSTALL_PACKAGES(动态申请) |
系统弹窗,需手动确认 | 所有第三方应用 |
核心结论:
- 如果你不是系统应用,直接看第三节"普通安装"即可,静默安装那部分了解就行。
- Android 9.0+ 对
pm install限制更严,非系统 UID 会直接拒绝。
二、权限声明(AndroidManifest.xml)
xml
<
<
三、核心工具类(完整生产代码)
ini
public class ApkInstallUtil {
private static final String TAG = "ApkInstallUtil";
/**
* 统一入口:优先静默安装,无权限则降级为普通安装
*/
public static void installApp(Context context, String filePath) {
if (context == null || TextUtils.isEmpty(filePath)) {
AppUtil.toast("安装参数异常");
return;
}
try {
// 只有系统应用才能拿到 INSTALL_PACKAGES 权限
if (ContextCompat.checkSelfPermission(context, Manifest.permission.INSTALL_PACKAGES)
== PackageManager.PERMISSION_GRANTED) {
silentInstall(context, filePath);
} else {
normalInstall(context, filePath);
}
} catch (Exception e) {
normalInstall(context, filePath);
}
}
/*================== 静默安装区域(系统应用专用) ==================*/
private static void silentInstall(Context context, String filePath) throws Exception {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
installApkByPm(filePath);
} else {
installApkBySession(context, filePath);
}
}
/**
* Android 5.0 之前:通过 pm install 命令安装
*/
private static boolean installApkByPm(String apkPath) {
String[] args = {"pm", "install", "-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 s;
while ((s = successResult.readLine()) != null) {
successMsg.append(s);
}
while ((s = errorResult.readLine()) != null) {
errorMsg.append(s);
}
return process.waitFor() == 0 || successMsg.toString().contains("Success");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
closeQuietly(successResult);
closeQuietly(errorResult);
if (process != null) {
process.destroy();
}
}
return false;
}
/**
* Android 5.0+:通过 PackageInstaller API 安装
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private static void installApkBySession(Context context, String apkFilePath) throws Exception {
PackageManager packageManager = context.getPackageManager();
File apkFile = new File(apkFilePath);
PackageInstaller packageInstaller = packageManager.getPackageInstaller();
if (packageInstaller == null) return;
PackageInstaller.SessionParams sessionParams =
new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
sessionParams.setSize(apkFile.length());
int sessionId = createSession(packageInstaller, sessionParams);
new Thread(() -> {
Looper.prepare();
packageInstaller.registerSessionCallback(new InstallSessionCallback(sessionId));
if (sessionId != -1) {
boolean copySuccess = copyInstallFile(packageInstaller, sessionId, apkFilePath);
if (copySuccess) {
execInstallCommand(context, packageInstaller, sessionId);
}
}
Looper.loop(); // 必须 loop,否则 SessionCallback 无法回调
}).start();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private static int createSession(PackageInstaller packageInstaller,
PackageInstaller.SessionParams sessionParams) {
try {
return packageInstaller.createSession(sessionParams);
} catch (IOException e) {
e.printStackTrace();
return -1;
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private static boolean copyInstallFile(PackageInstaller packageInstaller,
int sessionId, String apkFilePath) {
InputStream in = null;
OutputStream out = null;
PackageInstaller.Session session = null;
try {
File apkFile = new File(apkFilePath);
session = packageInstaller.openSession(sessionId);
out = session.openWrite("base.apk", 0, apkFile.length());
in = new FileInputStream(apkFile);
int c;
byte[] buffer = new byte[65536];
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
session.fsync(out);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
closeQuietly(out);
closeQuietly(in);
closeQuietly(session);
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private static void execInstallCommand(Context context,
PackageInstaller packageInstaller, int sessionId) {
PackageInstaller.Session session = null;
try {
session = packageInstaller.openSession(sessionId);
Intent intent = new Intent(context, InstallReceiver.class);
// Android 12+ 必须加 FLAG_IMMUTABLE,否则抛 IllegalArgumentException
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, flags);
session.commit(pendingIntent.getIntentSender());
} catch (IOException e) {
e.printStackTrace();
} finally {
closeQuietly(session);
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public static class InstallSessionCallback extends PackageInstaller.SessionCallback {
private final int mSessionId;
public InstallSessionCallback(int sessionId) {
this.mSessionId = sessionId;
}
@Override
public void onCreated(int sessionId) {}
@Override
public void onBadgingChanged(int sessionId) {}
@Override
public void onActiveChanged(int sessionId, boolean active) {}
@Override
public void onProgressChanged(int sessionId, float progress) {
if (sessionId == mSessionId) {
LogUtil.e(TAG, "安装进度: " + progress);
}
}
@Override
public void onFinished(int sessionId, boolean success) {
if (mSessionId == sessionId) {
LogUtil.e(TAG, success ? "静默安装成功" : "静默安装失败");
}
}
}
/*================== 普通安装区域(所有应用可用) ==================*/
/**
* 普通安装:系统弹窗,用户手动确认
*/
public static void normalInstall(Context context, String filePath) {
File file = new File(filePath);
if (!file.exists()) {
AppUtil.toast("安装包不存在");
return;
}
// Android 8.0+ 必须先检查是否有"安装未知应用"权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean canInstall = context.getPackageManager().canRequestPackageInstalls();
if (!canInstall) {
// 引导用户去设置页开启权限
Intent settingIntent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
.setData(Uri.parse("package:" + context.getPackageName()));
context.startActivity(settingIntent);
return;
}
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 7.0+ 必须使用 FileProvider,直接传 File URI 会抛 FileUriExposedException
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(context,
BuildConfig.APPLICATION_ID + ".fileProvider", file);
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
// 注册安装完成监听,用于安装成功后删除安装包
InstallCompleteReceiver receiver = new InstallCompleteReceiver(file);
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
context.registerReceiver(receiver, filter);
try {
context.startActivity(intent);
} catch (Throwable e) {
AppUtil.toast("安装出错或无权限");
LogUtil.e(TAG, "normalInstall error: " + e);
// 启动失败时立即注销广播,防止内存泄漏
try {
context.unregisterReceiver(receiver);
} catch (Exception ignored) {}
}
}
/**
* 安装完成广播:匹配包名后删除安装包,并注销自身
*/
public static class InstallCompleteReceiver extends BroadcastReceiver {
private final File apkFile;
public InstallCompleteReceiver(File apkFile) {
this.apkFile = apkFile;
}
@Override
public void onReceive(Context context, Intent intent) {
String packageName = intent.getData().getSchemeSpecificPart();
String installingPkg = getPackageName(context, apkFile.getAbsolutePath());
if (packageName != null && packageName.equals(installingPkg)) {
try {
if (apkFile.exists()) apkFile.delete();
} catch (Exception e) {
LogUtil.e(TAG, "删除安装包失败: " + e.getMessage());
} finally {
try {
context.unregisterReceiver(this);
} catch (Exception ignored) {}
}
}
}
}
/*================== 公共工具方法 ==================*/
/**
* 通过 APK 文件路径解析包名
*/
public static String getPackageName(Context context, String filePath) {
PackageManager pm = context.getPackageManager();
PackageInfo info = pm.getPackageArchiveInfo(filePath, PackageManager.GET_ACTIVITIES);
return info != null ? info.applicationInfo.packageName : null;
}
private static void closeQuietly(Closeable c) {
if (c != null) {
try {
c.close();
} catch (IOException ignored) {}
}
}
}
四、FileProvider 配置(普通安装必备,Android 7.0+)
很多开发者在这里踩坑,直接传 Uri.fromFile() 在 7.0+ 会直接崩溃。
AndroidManifest.xml:
ini
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
res/xml/file_paths.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="." />
<cache-path name="cache_files" path="." />
<!-- 根据你实际存放 APK 的路径补充,例如: -->
<files-path name="internal_files" path="." />
</paths>
五、兼容性验证记录
| 系统版本 | 测试环境 | 静默安装 | 普通安装 | 备注 |
|---|---|---|---|---|
| Android 4.4 | TV 盒子真机 | ✅ pm install |
✅ | 需 Root 或系统签名 |
| Android 7.1 | TV 盒子真机 | ✅ PackageInstaller | ✅ | |
| Android 10 | 教育平板真机 | ❌ 非系统 UID 拒绝 | ✅ | |
| Android 12 | 模拟器 | ❌ | ✅ | PendingIntent 必须加 FLAG_IMMUTABLE |
六、总结
- 先判断身份:不是系统应用就不要折腾静默安装,直接走普通安装。
- Android 7.0+ 必须用 FileProvider ,否则
FileUriExposedException教你做人。 - Android 8.0+ 普通安装前必须检查
canRequestPackageInstalls(),否则startActivity没反应。 - Android 12+ 的
PendingIntent必须加FLAG_IMMUTABLE,否则直接崩溃。 - PackageInstaller 的 SessionCallback 必须跑在带
Looper的线程里,否则安装完成了也收不到回调。