Android 文件下载(1)

在 Android 开发中,文件下载是网络编程的常见需求,通常通过 HTTP GET 请求从服务器下载文件(如图片、音频、视频、PDF 等)并保存到设备存储中。Android 提供了多种方式实现文件下载,包括原生的 HttpURLConnection 和第三方库如 OkHttpRetrofit。以下是 Android 文件下载的详细学习指南,重点介绍使用 HttpURLConnection,并涵盖代码示例、注意事项和实践建议。


一、文件下载概述

  • 目标:从服务器下载文件并保存到本地存储(如内部存储或 SD 卡)。
  • 常见场景
  • 下载图片、视频、APK 文件或文档。
  • 支持断点续传(Range 请求)。
  • 显示下载进度(大文件下载)。
  • 协议:通常使用 HTTP GET 请求,结合 Content-LengthRange 头。
  • 工具
  • HttpURLConnection:Android 原生,适合简单下载。
  • OkHttp:支持异步和进度监听,性能优越。
  • Retrofit:适合 RESTful API 下载,简化代码。
  • DownloadManager:系统提供的下载管理器,适合简单场景。
  • 关键点
  • 异步处理,避免阻塞主线程。
  • 处理文件存储权限(Android 6.0+)。
  • 支持大文件下载和进度反馈。
  • 确保资源释放和错误处理。

二、准备工作

  1. 权限声明
    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 或指定路径保存文件。
  1. 文件保存路径
  • 内部存储context.getFilesDir()context.getCacheDir()
  • 外部存储/sdcard/Download/Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
  • MediaStore(Android 10+):用于图片、视频等媒体文件。
  1. 服务器要求
  • 提供文件的下载 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");
  • 说明
  • 自动显示下载通知。
  • 不支持自定义进度监听或断点续传。
  • 适合简单下载任务。

五、注意事项

  1. 异步处理
  • 下载操作必须在子线程执行,避免 NetworkOnMainThreadException.
  • 使用 Thread、AsyncTask(已废弃)或 Kotlin 协程。
  1. 权限管理
  • 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);
  1. 错误处理
  • 处理 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); }
  1. 资源释放
  • 关闭 InputStreamOutputStreamHttpURLConnection
  1. 文件大小与性能
  • 检查 Content-Length 是否有效(可能为 -1)。
  • 使用缓冲区(如 4KB)读取流,优化性能。
  • 对于大文件,考虑断点续传或分片下载。
  1. 安全性
  • 使用 HTTPS 确保数据安全。
  • 验证服务器证书,避免信任所有证书。
  • 添加认证头(如 Authorization: Bearer token)。

六、学习建议与实践

  1. 学习路径
  • 了解 HTTP GET 请求和 Content-LengthRange 头。
  • 掌握 HttpURLConnection 基本下载流程。
  • 实现进度监听和断点续传。
  • 学习 OkHttp 或 Retrofit 替代 HttpURLConnection
  • 探索 DownloadManager 简单场景。
  1. 实践项目
  • 简单项目:下载图片并保存到 Downloads 目录。
  • 进阶项目:实现视频下载,支持进度显示。
  • 高级项目:支持断点续传,保存下载进度。
  1. 调试工具
  • 使用 Postman 测试下载 URL,验证响应。
  • 使用 Charles 或 Fiddler 抓包,分析 HTTP 请求。
  • 使用 Android Studio 的 Network Profiler 查看请求详情.
  1. 推荐资源
  • 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 安全。
  • 推荐:简单场景使用 HttpURLConnectionDownloadManager,复杂场景切换到 OkHttp/Retrofit。

如果需要更详细的代码示例(如断点续传实现、MediaStore 存储)或 OkHttp/Retrofit 下载方法,请告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注