4. 下载升级包并校验MD5
任务说明
本案例实现Android端OTA升级包下载与完整性校验闭环:从指定URL下载升级包到应用私有目录,下载完成后计算文件MD5值,与服务器下发的期望值比对,确保升级包未被篡改或损坏。核心代码可直接嵌入真实OTA流程中的下载与校验环节。
环境依赖
无需第三方库,基于HttpURLConnection与MessageDigest实现。需在AndroidManifest.xml中声明INTERNET权限,若targetSdkVersion≥28还需在application节点设置android:usesCleartextTraffic="true"(仅当使用http协议时)。
关键完整代码
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
public class OtaDownloader {
private static final String TAG = "OtaDownloader";
private static final int BUFFER_SIZE = 8192;
private static final int TIMEOUT_MS = 30000;
public interface DownloadCallback {
void onSuccess(File file, String actualMd5);
void onFailure(String errorMsg);
}
/**
* 下载升级包并校验MD5
*
* @param context 上下文(用于获取私有目录)
* @param downloadUrl 升级包下载地址
* @param expectedMd5 服务器下发的期望MD5值(32位十六进制)
* @param callback 结果回调
*/
public static void downloadAndVerify(Context context, String downloadUrl,
String expectedMd5, DownloadCallback callback) {
new Thread(() -> {
File outputFile = null;
try {
// 1. 创建输出文件(应用私有目录,避免存储权限)
File dir = new File(context.getFilesDir(), "ota");
if (!dir.exists() && !dir.mkdirs()) {
callback.onFailure("创建目录失败");
return;
}
outputFile = new File(dir, "update.zip");
// 2. 下载文件
URL url = new URL(downloadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS);
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
callback.onFailure("HTTP错误: " + responseCode);
conn.disconnect();
return;
}
// 3. 边下载边计算MD5(避免二次读取)
MessageDigest md5Digest = MessageDigest.getInstance("MD5");
try (InputStream is = conn.getInputStream();
FileOutputStream fos = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
md5Digest.update(buffer, 0, bytesRead);
}
fos.flush();
}
conn.disconnect();
// 4. 比对MD5
String actualMd5 = bytesToHex(md5Digest.digest());
if (expectedMd5 == null || expectedMd5.isEmpty()) {
callback.onFailure("期望MD5为空,无法校验");
return;
}
if (!expectedMd5.equalsIgnoreCase(actualMd5)) {
callback.onFailure("MD5校验失败,期望: " + expectedMd5
+ " 实际: " + actualMd5);
return;
}
callback.onSuccess(outputFile, actualMd5);
} catch (Exception e) {
Log.e(TAG, "下载或校验异常", e);
// 清理不完整文件
if (outputFile != null && outputFile.exists()) {
outputFile.delete();
}
callback.onFailure("异常: " + e.getMessage());
}
}).start();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
调用方式
OtaDownloader.downloadAndVerify(context,
"https://example.com/ota/update_v1.2.zip",
"e4b99a8f8c6d2e1a0b3f5c7d9e2a4b6c",
new OtaDownloader.DownloadCallback() {
@Override
public void onSuccess(File file, String actualMd5) {
// 文件已就绪且MD5匹配,可执行安装流程
Log.i("OTA", "下载成功: " + file.getAbsolutePath());
}
@Override
public void onFailure(String errorMsg) {
Log.e("OTA", "失败: " + errorMsg);
}
});
验证方法
在测试设备上准备一个已知内容的文件(如10MB随机数据),用md5sum命令计算其MD5值作为expectedMd5,通过本地HTTP服务(如Python的http.server)提供下载。运行上述代码后,观察日志:
- 通过条件:日志输出“下载成功”,且文件路径指向的文件存在,其MD5与期望值一致。
- 失败验证:将期望MD5改错一位,应输出“MD5校验失败”,且目标文件被删除(无残留)。
若下载中断(如飞行模式),应输出“异常”或“HTTP错误”,且无残留文件。
常见问题
- 下载大文件内存溢出:本实现使用流式写入与增量MD5更新,内存占用恒定,不会因文件增大而溢出。
- 校验失败但文件已存在:代码在异常和校验失败路径均会删除不完整文件,避免脏数据残留。
- 主线程网络异常:下载已在子线程执行,回调回到调用线程(主线程),可直接更新UI。