Android 文件下载(1)
在 Android 开发中,文件下载是网络编程的常见需求,通常通过 HTTP GET 请求从服务器下载文件(如图片、音频、视频、PDF 等)并保存到设备存储中。Android 提供了多种方式实现文件下载,包括原生的 HttpURLConnection
和第三方库如 OkHttp 和 Retrofit。以下是 Android 文件下载的详细学习指南,重点介绍使用 HttpURLConnection
,并涵盖代码示例、注意事项和实践建议。
一、文件下载概述
- 目标:从服务器下载文件并保存到本地存储(如内部存储或 SD 卡)。
- 常见场景:
- 下载图片、视频、APK 文件或文档。
- 支持断点续传(Range 请求)。
- 显示下载进度(大文件下载)。
- 协议:通常使用 HTTP GET 请求,结合
Content-Length
和Range
头。 - 工具:
- HttpURLConnection:Android 原生,适合简单下载。
- OkHttp:支持异步和进度监听,性能优越。
- Retrofit:适合 RESTful API 下载,简化代码。
- DownloadManager:系统提供的下载管理器,适合简单场景。
- 关键点:
- 异步处理,避免阻塞主线程。
- 处理文件存储权限(Android 6.0+)。
- 支持大文件下载和进度反馈。
- 确保资源释放和错误处理。
二、准备工作
- 权限声明:
在AndroidManifest.xml
中添加网络和存储权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 9 及以下需要 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- Android 6.0+:动态请求存储权限。
- Android 10+:推荐使用
Scoped Storage
,通过MediaStore
或指定路径保存文件。
- 文件保存路径:
- 内部存储:
context.getFilesDir()
或context.getCacheDir()
。 - 外部存储:
/sdcard/Download/
或Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
。 - MediaStore(Android 10+):用于图片、视频等媒体文件。
- 服务器要求:
- 提供文件的下载 URL(如
https://example.com/file.jpg
)。 - 支持
Range
请求头(用于断点续传)。 - 返回
Content-Length
头以获取文件大小。
三、使用 HttpURLConnection 实现文件下载
HttpURLConnection
是 Android 原生的 HTTP 客户端,适合简单文件下载。以下是详细实现步骤和代码示例。
1. 基本文件下载
下载文件并保存到本地存储。
- 代码示例:
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class FileDownloader {
public static String downloadFile(String fileUrl, String savePath) {
HttpURLConnection conn = null;
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
// 1. 创建连接
URL url = new URL(fileUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestProperty("Accept", "*/*");
// 2. 检查响应
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 3. 获取输入流
inputStream = conn.getInputStream();
File file = new File(savePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
// 4. 保存文件
outputStream = new FileOutputStream(file);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
return "Download completed: " + savePath;
} else {
return "Error: HTTP " + responseCode;
}
} catch (Exception e) {
e.printStackTrace();
return "Exception: " + e.getMessage();
} finally {
try {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
if (conn != null) conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
- 使用示例(异步调用):
new Thread(() -> {
String fileUrl = "https://example.com/image.jpg";
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/image.jpg";
String result = FileDownloader.downloadFile(fileUrl, savePath);
runOnUiThread(() -> {
Log.d("Download", result);
});
}).start();
- 说明:
- 使用
GET
请求获取文件流。 - 将输入流写入本地文件。
- 确保在子线程执行网络操作。
2. 下载进度监听
对于大文件,显示下载进度是常见需求。
- 代码示例:
public class FileDownloader {
public interface ProgressListener {
void onProgress(int progress); // 进度百分比
}
public static String downloadFileWithProgress(String fileUrl, String savePath, ProgressListener listener) {
HttpURLConnection conn = null;
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
URL url = new URL(fileUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 获取文件总大小
long totalSize = conn.getContentLengthLong();
inputStream = conn.getInputStream();
File file = new File(savePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
outputStream = new FileOutputStream(file);
byte[] buffer = new byte[4096];
int bytesRead;
long downloadedSize = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
downloadedSize += bytesRead;
if (totalSize > 0) {
int progress = (int) (downloadedSize * 100 / totalSize);
if (listener != null) {
listener.onProgress(progress);
}
}
}
outputStream.flush();
return "Download completed: " + savePath;
} else {
return "Error: HTTP " + responseCode;
}
} catch (Exception e) {
e.printStackTrace();
return "Exception: " + e.getMessage();
} finally {
try {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
if (conn != null) conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
- 使用示例:
new Thread(() -> {
String fileUrl = "https://example.com/video.mp4";
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/video.mp4";
String result = FileDownloader.downloadFileWithProgress(fileUrl, savePath, progress -> {
runOnUiThread(() -> {
Log.d("Download", "Progress: " + progress + "%");
// 更新 UI,例如 ProgressBar
});
});
runOnUiThread(() -> {
Log.d("Download", result);
});
}).start();
- 说明:
- 通过
conn.getContentLengthLong()
获取文件大小。 - 计算下载进度(
downloadedSize / totalSize
)。 - 通过回调通知 UI 线程更新进度。
3. 断点续传
断点续传通过 Range
请求头继续下载未完成的部分。
- 代码示例:
public class FileDownloader {
public static String downloadFileWithResume(String fileUrl, String savePath, ProgressListener listener) {
HttpURLConnection conn = null;
InputStream inputStream = null;
RandomAccessFile randomAccessFile = null;
File file = new File(savePath);
long downloadedSize = file.exists() ? file.length() : 0;
try {
URL url = new URL(fileUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
// 设置 Range 头
if (downloadedSize > 0) {
conn.setRequestProperty("Range", "bytes=" + downloadedSize + "-");
}
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
long totalSize = conn.getContentLengthLong() + downloadedSize;
inputStream = conn.getInputStream();
randomAccessFile = new RandomAccessFile(file, "rw");
randomAccessFile.seek(downloadedSize);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, bytesRead);
downloadedSize += bytesRead;
if (totalSize > 0) {
int progress = (int) (downloadedSize * 100 / totalSize);
if (listener != null) {
listener.onProgress(progress);
}
}
}
return "Download completed: " + savePath;
} else {
return "Error: HTTP " + responseCode;
}
} catch (Exception e) {
e.printStackTrace();
return "Exception: " + e.getMessage();
} finally {
try {
if (inputStream != null) inputStream.close();
if (randomAccessFile != null) randomAccessFile.close();
if (conn != null) conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
- 使用示例:
new Thread(() -> {
String fileUrl = "https://example.com/video.mp4";
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/video.mp4";
String result = FileDownloader.downloadFileWithResume(fileUrl, savePath, progress -> {
runOnUiThread(() -> {
Log.d("Download", "Progress: " + progress + "%");
});
});
runOnUiThread(() -> {
Log.d("Download", result);
});
}).start();
- 说明:
- 使用
Range
头指定下载起始位置(bytes=downloadedSize-
)。 - 使用
RandomAccessFile
支持追加写入。 - 服务器需支持
206 Partial Content
响应。
四、结合 DownloadManager(简单场景)
Android 提供的 DownloadManager
是系统级下载工具,适合简单文件下载。
- 代码示例:
import android.app.DownloadManager;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
public void downloadWithDownloadManager(Context context, String fileUrl, String fileName) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(fileUrl))
.setTitle(fileName)
.setDescription("Downloading file")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
downloadManager.enqueue(request);
}
- 使用示例:
downloadWithDownloadManager(this, "https://example.com/image.jpg", "image.jpg");
- 说明:
- 自动显示下载通知。
- 不支持自定义进度监听或断点续传。
- 适合简单下载任务。
五、注意事项
- 异步处理:
- 下载操作必须在子线程执行,避免
NetworkOnMainThreadException
. - 使用 Thread、AsyncTask(已废弃)或 Kotlin 协程。
- 权限管理:
- Android 6.0+:动态请求
WRITE_EXTERNAL_STORAGE
权限。 - Android 10+:优先使用
MediaStore
或内部存储:java ContentValues values = new ContentValues(); values.put(MediaStore.Downloads.DISPLAY_NAME, "image.jpg"); values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); Uri uri = context.getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
- 错误处理:
- 处理
IOException
和 HTTP 错误状态码(4xx、5xx)。 - 示例:
java if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_PARTIAL) { InputStream errorStream = conn.getErrorStream(); String error = new BufferedReader(new InputStreamReader(errorStream)).lines().collect(Collectors.joining("\n")); Log.e("Error", error); }
- 资源释放:
- 关闭
InputStream
、OutputStream
和HttpURLConnection
。
- 文件大小与性能:
- 检查
Content-Length
是否有效(可能为 -1)。 - 使用缓冲区(如 4KB)读取流,优化性能。
- 对于大文件,考虑断点续传或分片下载。
- 安全性:
- 使用 HTTPS 确保数据安全。
- 验证服务器证书,避免信任所有证书。
- 添加认证头(如
Authorization: Bearer token
)。
六、学习建议与实践
- 学习路径:
- 了解 HTTP GET 请求和
Content-Length
、Range
头。 - 掌握
HttpURLConnection
基本下载流程。 - 实现进度监听和断点续传。
- 学习 OkHttp 或 Retrofit 替代
HttpURLConnection
。 - 探索
DownloadManager
简单场景。
- 实践项目:
- 简单项目:下载图片并保存到
Downloads
目录。 - 进阶项目:实现视频下载,支持进度显示。
- 高级项目:支持断点续传,保存下载进度。
- 调试工具:
- 使用 Postman 测试下载 URL,验证响应。
- 使用 Charles 或 Fiddler 抓包,分析 HTTP 请求。
- 使用 Android Studio 的 Network Profiler 查看请求详情.
- 推荐资源:
- Android 官方文档:https://developer.android.com/reference/java/net/HttpURLConnection
- MDN HTTP 文档:https://developer.mozilla.org/en-US/docs/Web/HTTP
- 实践 URL:测试文件下载(如 https://speedtest.tele2.net/1MB.zip)
七、总结
- HttpURLConnection:Android 原生,适合简单文件下载。
- 核心步骤:配置 GET 请求、读取输入流、保存文件、释放资源。
- 功能扩展:
- 进度监听:通过
Content-Length
计算进度。 - 断点续传:使用
Range
头和RandomAccessFile
。 - 注意事项:异步处理、权限管理、错误处理、资源释放、HTTPS 安全。
- 推荐:简单场景使用
HttpURLConnection
或DownloadManager
,复杂场景切换到 OkHttp/Retrofit。
如果需要更详细的代码示例(如断点续传实现、MediaStore 存储)或 OkHttp/Retrofit 下载方法,请告诉我!