Android 自动更新:从下载 APK 到安装的全流程
引言
在 Android 上做自动更新,没有 iOS 的 APNs,只能自己搭一套。我用了比较原始但可靠的全量 APK 下载方案,记录一下关键步骤和坑。
权限配置
先得在 manifest 里声明权限。INTERNET 不用多说。存储权限 Android 6.0 以后需要动态申请,不然文件落不了地。安装未知来源权限 Android 8.0 起也需要运行时请求。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="lgx.acc.updatedemo">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 存储权限 (Android 6.0+ 需动态申请) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 安装权限 (Android 8.0+ 需动态申请) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>
动态申请存储权限的逻辑就不展开了,网上有很多,这里记住一点:如果没申请成功,下载会悄无声息失败。
核心思路
设计上就几条:下载走子线程,UI 刷新回主线程,用 Handler 传消息,外加一个取消标志。UI 这边用 Dialog 展示进度,体验比通知栏好控制。
实现步骤
检查更新
先得有个地方判断版本号。实际项目里会请求服务端接口,这里先简化成变量。
public void checkUpdateInfo() {
// 请求服务端接口,拿到最新版本信息后与本地比较
boolean isNew = false; // 假设需要更新
if (!isNew) {
showUpdateDialog();
}
}
更新提示弹窗
发现有新版就弹窗,给'下载'和'以后再说'按钮。这里直接用 AlertDialog 就行。
private void showUpdateDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle("软件版本更新");
builder.setMessage("发现新版本,是否立即下载?");
builder.setPositiveButton("下载", (dialog, which) -> {
showDownloadDialog();
});
builder.setNegativeButton("以后再说", (dialog, which) -> {
dialog.dismiss();
});
builder.create().show();
}
下载进度 Dialog
自定义一个带进度条的 XML 布局,放在 Dialog 里。布局文件 progress.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/text_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="下载进度:0%"
android:textSize="14sp" />
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progress="0" />
</LinearLayout>
下载任务
下载跑在 Runnable 里,用 HttpURLConnection。注意 read timeout 别设太长,不然取消时要等很久。进度计算加了个 content-length 未知时的处理,防止除零。
private Runnable mdownApkRunnable = new Runnable() {
@Override
public void run() {
URL url;
try {
// 建议使用 HTTPS 地址以保证安全
url = new URL("https://example.com/app/release.apk");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.connect();
int length = conn.getContentLength();
InputStream ins = conn.getInputStream();
File file = new File(savePath);
if (!file.exists()) {
file.mkdirs();
}
File apkFile = new File(saveFileName);
FileOutputStream fos = new FileOutputStream(apkFile);
int count = 0;
byte[] buf = new byte[1024];
int numread;
while (!intercept && (numread = ins.read(buf)) != -1) {
count += numread;
// 防止除以零错误
if (length > 0) {
progress = (int) (((float) count / length) * 100);
} else {
progress = 100;
}
// 通知 UI 刷新进度
mHandler.sendEmptyMessage(DOWN_UPDATE);
fos.write(buf, 0, numread);
}
fos.close();
ins.close();
conn.disconnect();
if (!intercept) {
// 下载完成通知安装
mHandler.sendEmptyMessage(DOWN_OVER);
}
} catch (Exception e) {
e.printStackTrace();
// 处理异常,例如显示错误提示
mHandler.sendEmptyMessage(DOWN_ERROR);
}
}
};
启动线程并处理好 Handler 消息,更新进度和下载完成状态。
安装 APK
下载完直接调安装 Intent。Android 7.0 以上会报 FileUriExposedException,必须用 FileProvider 转 content URI。实际项目里直接照搬 FileProvider 配置即可。
private void installAPK() {
File apkFile = new File(saveFileName);
if (!apkFile.exists()) return;
Intent intent = new Intent(Intent.ACTION_VIEW);
// 7.0 以上需用 FileProvider,此处为示意
intent.setDataAndType(Uri.parse("file://" + apkFile.toString()),
"application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
踩坑与建议
网络请求
生产环境一定用 HTTPS,并且最好在下载完成后校验一下签名,防止包被替换。
兼容性
Android 11 分区存储后,WRITE_EXTERNAL_STORAGE 基本废弃,建议用 DownloadManager 或 MediaStore。不过对于全量更新 APK,很多人还是用外部存储的公有目录,短期还能用。
DownloadManager
如果只是想简单把下载丢给系统,DownloadManager 最省心,可以后台下载并显示通知。缺点是没法自定义进度 UI,对更新流程控制弱一些。
// DownloadManager 示例思路
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
request.setTitle("App Update");
request.setDescription("Downloading update...");
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(request);
总结
这套基于 HttpURLConnection + Handler + Dialog 的方案,适合快速理解自动更新的内部流程。真正线上项目我更推荐 OkHttp 处理下载,同时结合 DownloadManager 做保底。再提醒一下:各版本权限差异一定要处理,不然用户点完更新就 crash,留存都跌没了。


