一、项目介绍
在移动应用的全生命周期中,版本迭代和用户更新体验至关重要。传统的做法是依赖 Google Play 商店强制推送更新,但在某些场景下,我们需要:
-
更即时地控制更新流程(如灰度、强制升级、提醒升级等);
-
支持市场外分发,比如企业内部应用分发、第三方应用商店;
-
自定义更新 UI,与应用风格保持一致。
本项目示例将展示两种主流方案:
-
Google Play In‑App Updates(官方方案,适用于上架 Play 商店的应用)
-
自建服务 + APK 下载 & 安装(适用于非 Play 分发场景)
通过本教程,你将学会如何在应用内检测新版本、弹出升级对话框、后台下载 APK、以及无缝触发安装流程,极大提升用户体验。
二、相关知识
-
Google Play Core Library
-
com.google.android.play:core:1.x.x
包含了 In‑App Updates API,让应用可在运行时检查并触发“灵活更新”或“立即更新”流程,无需用户去 Play 商店界面。
-
-
FileProvider & 安装意图
-
对于自建更新方案,需要在
AndroidManifest.xml
配置FileProvider
,并通过Intent.ACTION_VIEW
携带 APK 的content://
URI,调用系统安装界面。
-
-
WorkManager / DownloadManager
-
长任务(如后台下载 APK)应使用
WorkManager
或系统DownloadManager
,保证下载可在后台稳定运行,且重启后可续传。
-
-
运行时权限 & 兼容性
-
Android 8.0+(API 26+)安装需获取 “允许安装未知应用” 权限 (
REQUEST_INSTALL_PACKAGES
)。 -
Android 7.0+(API 24+)文件 URI 必须走
FileProvider
,否则会抛FileUriExposedException
。
-
三、项目实现思路
-
版本检测
-
Play 方案:调用 Play Core 的
AppUpdateManager.getAppUpdateInfo()
检查更新状态。 -
自建方案:向自有服务器发起网络请求(如 GET
/latest_version.json
),获取最新版本号、APK 下载地址、更新说明等。
-
-
弹窗交互
-
根据策略选择“立即更新”(强制)或“灵活更新”(允许后台运行时再重启安装),并展示更新日志。
-
-
下载 APK
-
Play 方案:由 Play Core 自动下载。
-
自建方案:用
DownloadManager
启动下载,并监听广播获取下载完成通知。
-
-
触发安装
-
下载完成后,构造
Intent.ACTION_VIEW
,指定 MIME 类型application/vnd.android.package-archive
,使用FileProvider
共享 APK URI,启动安装流程。
-
四、完整代码(All‑in‑One,含详细注释)
// =======================================
// 文件: AutoUpdateManager.java + MainActivity
// (本示例将 Manager 与 Activity 合写于一处,注释区分)
// =======================================
package com.example.autoupdate;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
// —— Play Core 库依赖(立即更新/灵活更新)
// implementation "com.google.android.play:core:1.10.3"
import com.google.android.play.core.appupdate.AppUpdateInfo;
import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
import com.google.android.play.core.install.model.AppUpdateType;
import com.google.android.play.core.install.model.UpdateAvailability;
import com.google.android.play.core.tasks.Task;
import org.json.JSONObject;
import java.io.File;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
public class MainActivity extends AppCompatActivity {
// ---------- 常量区 ----------
private static final int REQUEST_CODE_UPDATE = 100; // Play 更新请求码
private static final int REQUEST_INSTALL_PERMISSION = 101; // 动态安装权限
private static final String TAG = "AutoUpdate";
private long downloadId; // DownloadManager 返回 ID
private DownloadManager downloadManager;
// ---------- onCreate ----------
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 布局见下文
// 按钮触发两种更新
Button btnPlayUpdate = findViewById(R.id.btnPlayUpdate);
Button btnCustomUpdate = findViewById(R.id.btnCustomUpdate);
btnPlayUpdate.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
checkPlayUpdate(); // Play 商店内更新
}
});
btnCustomUpdate.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
checkCustomUpdate(); // 自建服务器更新
}
});
// 初始化 DownloadManager
downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
// 注册下载完成广播
registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
// ---------- 1. Play In‑App Updates 检查 ----------
private void checkPlayUpdate() {
// 创建 AppUpdateManager
com.google.android.play.core.appupdate.AppUpdateManager appUpdateManager =
AppUpdateManagerFactory.create(this);
// 异步获取更新信息
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();
appUpdateInfoTask.addOnSuccessListener(info -> {
// 判断是否有更新且支持立即更新
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
try {
// 发起灵活更新请求
appUpdateManager.startUpdateFlowForResult(
info,
AppUpdateType.FLEXIBLE,
this,
REQUEST_CODE_UPDATE);
} catch (Exception e) {
Log.e(TAG, "Play 更新启动失败", e);
}
} else {
Toast.makeText(this, "无可用更新或不支持此更新类型", Toast.LENGTH_SHORT).show();
}
});
}
// ---------- 2. 自建服务器版本检测 ----------
private void checkCustomUpdate() {
new Thread(() -> {
try {
// 1) 请求服务器 JSON
URL url = new URL("https://your.server.com/latest_version.json");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
InputStream in = conn.getInputStream();
Scanner sc = new Scanner(in).useDelimiter("\\A");
String json = sc.hasNext() ? sc.next() : "";
JSONObject obj = new JSONObject(json);
final int serverVersionCode = obj.getInt("versionCode");
final String apkUrl = obj.getString("apkUrl");
final String changeLog = obj.getString("changeLog");
// 2) 获取本地版本号
int localVersionCode = getPackageManager()
.getPackageInfo(getPackageName(), 0).versionCode;
if (serverVersionCode > localVersionCode) {
// 有新版,回到主线程弹窗提示
runOnUiThread(() ->
showUpdateDialog(apkUrl, changeLog)
);
} else {
runOnUiThread(() ->
Toast.makeText(this, "已是最新版本", Toast.LENGTH_SHORT).show()
);
}
} catch (Exception e) {
Log.e(TAG, "检查更新失败", e);
}
}).start();
}
// ---------- 3. 弹出更新对话框 ----------
private void showUpdateDialog(String apkUrl, String changeLog) {
new AlertDialog.Builder(this)
.setTitle("发现新版本")
.setMessage(changeLog)
.setCancelable(false)
.setPositiveButton("立即更新", (dialog, which) -> {
startDownload(apkUrl);
})
.setNegativeButton("稍后再说", null)
.show();
}
// ---------- 4. 启动系统 DownloadManager 下载 APK ----------
private void startDownload(String apkUrl) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI
| DownloadManager.Request.NETWORK_MOBILE);
request.setTitle("正在下载更新包");
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "update.apk");
// 开始下载
downloadId = downloadManager.enqueue(request);
}
// ---------- 5. 监听下载完成,触发安装 ----------
private BroadcastReceiver onDownloadComplete = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id != downloadId) return;
// 下载完成,安装 APK
File apkFile = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "update.apk");
// Android 8.0+ 需要请求安装未知应用权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean canInstall = getPackageManager().canRequestPackageInstalls();
if (!canInstall) {
// 请求“安装未知应用”权限
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES},
REQUEST_INSTALL_PERMISSION);
return;
}
}
installApk(apkFile);
}
};
// ---------- 6. 处理未知来源权限申请结果 ----------
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == REQUEST_INSTALL_PERMISSION) {
if (grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED) {
// 再次触发安装(假设 APK 仍在下载目录)
File apkFile = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "update.apk");
installApk(apkFile);
} else {
Toast.makeText(this, "安装权限被拒绝,无法自动更新", Toast.LENGTH_LONG).show();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
// ---------- 7. 安装 APK 辅助方法 ----------
private void installApk(File apkFile) {
Uri apkUri = FileProvider.getUriForFile(this,
getPackageName() + ".fileprovider", apkFile);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, "无法启动安装程序", Toast.LENGTH_LONG).show();
}
}
// ---------- 8. Activity 销毁时注销 Receiver ----------
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(onDownloadComplete);
}
}
<!-- ======================================
文件: AndroidManifest.xml
注意:需要配置 FileProvider 与权限
====================================== -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.autoupdate">
<!-- 安装未知来源权限(Android 8.0+ 需动态申请) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- FileProvider 声明 -->
<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>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
<!-- ======================================
文件: res/xml/file_paths.xml
FileProvider 路径配置
====================================== -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="download" path="Download/"/>
</paths>
<!-- ======================================
文件: res/layout/activity_main.xml
简单示例界面
====================================== -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:gravity="center"
android:layout_width="match_parent" android:layout_height="match_parent"
android:padding="24dp">
<Button
android:id="@+id/btnPlayUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Play In‑App 更新"/>
<View android:layout_height="16dp" android:layout_width="match_parent"/>
<Button
android:id="@+id/btnCustomUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自建服务更新"/>
</LinearLayout>
五、方法解读
-
checkPlayUpdate()
检查 Google Play 上的更新可用性,并以“灵活更新”方式启动下载和安装流程。 -
checkCustomUpdate()
通过HttpURLConnection
请求服务器 JSON,解析最新versionCode
与apkUrl
,对比本地版本,决定是否弹窗。 -
showUpdateDialog(...)
基于服务器返回的changeLog
构建AlertDialog
,提供“立即更新”与“稍后再说”两种交互。 -
startDownload(String apkUrl)
使用系统DownloadManager
发起后台下载,保存至公开目录,支持断点续传和系统下载通知。 -
BroadcastReceiver onDownloadComplete
监听DownloadManager.ACTION_DOWNLOAD_COMPLETE
广播,确认是本次下载后触发安装流程。 -
onRequestPermissionsResult(...)
处理 Android 8.0+ “安装未知来源”权限授权结果,授权后继续调用installApk()
。 -
installApk(File apkFile)
通过FileProvider
获取 APK 的 content URI,并以Intent.ACTION_VIEW
调用系统安装器。
六、项目总结
优势
-
Play Core In‑App 更新:官方支持,体验与 Play 商店一致,无需手工管理下载逻辑。
-
自建方案:灵活可控,支持任意分发渠道,自定义 UI 与灰度策略。
注意与优化
-
权限与兼容
-
Android 7.0+ 必须使用
FileProvider
。 -
Android 8.0+ 需动态申请
REQUEST_INSTALL_PACKAGES
。
-
-
下载失败重试
-
可结合
WorkManager
增加重试与网络断线重连逻辑。
-
-
安全性
-
建议对 APK 做签名校验(计算 SHA256 与服务器比对),防止被篡改。
-
-
UI 体验
-
对“立即更新”与“后台更新”作更多状态提示。
-
可显示下载进度条、进度通知等。
-
-
灰度/强制升级
-
可在服务器 JSON 中添加策略字段,如
forceUpdate
,在对话框中禁止“稍后再说”。
-