Android 文件下载(2)

继上一部分(Android 文件下载(1))介绍了使用 HttpURLConnection 实现文件下载后,本部分将重点讲解使用 OkHttpRetrofit 进行文件下载,包括基本下载、进度监听和断点续传的实现。还将补充 DownloadManager 的高级用法,并提供更全面的注意事项和实践建议。本指南假设你已熟悉 HTTP GET 请求和文件存储基础,旨在深入探讨现代 Android 文件下载的实现方式。


一、文件下载概述(回顾)

  • 目标:从服务器下载文件(如图片、视频、APK)并保存到本地存储。
  • 关键点
  • 使用 HTTP GET 请求,结合 Content-LengthRange 头。
  • 异步处理,避免阻塞主线程。
  • 支持进度监听和断点续传。
  • 处理存储权限和 Android 10+ 的 Scoped Storage。
  • 工具
  • OkHttp:支持异步请求、进度监听、连接池,适合复杂场景。
  • Retrofit:基于 OkHttp,简化 RESTful API 下载。
  • DownloadManager:系统级工具,适合简单下载。
  • 场景
  • 基本下载:下载小文件(如图片)。
  • 高级需求:大文件下载、进度显示、断点续传。

二、准备工作(回顾)

  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 StorageMediaStore 保存文件。
  1. 依赖添加
    build.gradle(app 模块)中添加:
   // OkHttp
   implementation 'com.squareup.okhttp3:okhttp:4.12.0'
   // Retrofit
   implementation 'com.squareup.retrofit2:retrofit:2.11.0'
  1. 文件保存路径
  • 内部存储:context.getFilesDir()
  • 外部存储:Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
  • MediaStore(Android 10+):用于媒体文件。
  1. 服务器要求
  • 提供文件下载 URL。
  • 支持 Range 请求(断点续传)。

三、使用 OkHttp 实现文件下载

OkHttp 是一个功能强大的第三方 HTTP 客户端,支持异步请求、进度监听和断点续传,适合复杂下载需求。

1. 基本文件下载

下载文件并保存到本地存储。

  • 代码示例
  import okhttp3.OkHttpClient;
  import okhttp3.Request;
  import okhttp3.Response;
  import okio.BufferedSink;
  import okio.Okio;
  import java.io.File;

  public class OkHttpDownloader {
      public static String downloadFile(String fileUrl, String savePath) {
          OkHttpClient client = new OkHttpClient();
          Request request = new Request.Builder()
                  .url(fileUrl)
                  .get()
                  .addHeader("Accept", "*/*")
                  .build();

          try (Response response = client.newCall(request).execute()) {
              if (response.isSuccessful() && response.body() != null) {
                  File file = new File(savePath);
                  if (!file.getParentFile().exists()) {
                      file.getParentFile().mkdirs();
                  }
                  BufferedSink sink = Okio.buffer(Okio.sink(file));
                  sink.writeAll(response.body().source());
                  sink.close();
                  return "Download completed: " + savePath;
              } else {
                  return "Error: HTTP " + response.code();
              }
          } catch (Exception e) {
              e.printStackTrace();
              return "Exception: " + e.getMessage();
          }
      }
  }
  • 使用示例(异步调用):
  new Thread(() -> {
      String fileUrl = "https://example.com/image.jpg";
      String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/image.jpg";
      String result = OkHttpDownloader.downloadFile(fileUrl, savePath);
      runOnUiThread(() -> {
          Log.d("Download", result);
      });
  }).start();
  • 异步调用(OkHttp 回调):
  OkHttpClient client = new OkHttpClient();
  Request request = new Request.Builder()
          .url("https://example.com/image.jpg")
          .get()
          .build();
  client.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Call call, IOException e) {
          runOnUiThread(() -> Log.e("Download", "Exception: " + e.getMessage()));
      }

      @Override
      public void onResponse(Call call, Response response) throws IOException {
          if (response.isSuccessful() && response.body() != null) {
              File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/image.jpg");
              if (!file.getParentFile().exists()) {
                  file.getParentFile().mkdirs();
              }
              BufferedSink sink = Okio.buffer(Okio.sink(file));
              sink.writeAll(response.body().source());
              sink.close();
              runOnUiThread(() -> Log.d("Download", "Download completed"));
          } else {
              runOnUiThread(() -> Log.e("Download", "Error: HTTP " + response.code()));
          }
      }
  });
  • 说明
  • 使用 Okio 高效写入文件。
  • 支持异步回调,适合 UI 响应。
  • 自动管理连接池。

2. 下载进度监听

通过自定义 Interceptor 或包装 ResponseBody 实现进度监听。

  • 代码示例
  import okhttp3.Interceptor;
  import okhttp3.OkHttpClient;
  import okhttp3.Request;
  import okhttp3.Response;
  import okhttp3.ResponseBody;
  import okio.BufferedSink;
  import okio.Okio;
  import java.io.File;

  public class OkHttpDownloader {
      public interface ProgressListener {
          void onProgress(int progress);
      }

      public static String downloadFileWithProgress(String fileUrl, String savePath, ProgressListener listener) {
          OkHttpClient client = new OkHttpClient.Builder()
                  .addNetworkInterceptor(chain -> {
                      Response originalResponse = chain.proceed(chain.request());
                      return originalResponse.newBuilder()
                              .body(new ProgressResponseBody(originalResponse.body(), listener))
                              .build();
                  })
                  .build();

          Request request = new Request.Builder()
                  .url(fileUrl)
                  .get()
                  .build();

          try (Response response = client.newCall(request).execute()) {
              if (response.isSuccessful() && response.body() != null) {
                  File file = new File(savePath);
                  if (!file.getParentFile().exists()) {
                      file.getParentFile().mkdirs();
                  }
                  BufferedSink sink = Okio.buffer(Okio.sink(file));
                  sink.writeAll(response.body().source());
                  sink.close();
                  return "Download completed: " + savePath;
              } else {
                  return "Error: HTTP " + response.code();
              }
          } catch (Exception e) {
              e.printStackTrace();
              return "Exception: " + e.getMessage();
          }
      }

      private static class ProgressResponseBody extends ResponseBody {
          private final ResponseBody responseBody;
          private final ProgressListener listener;
          private BufferedSource bufferedSource;

          public ProgressResponseBody(ResponseBody responseBody, ProgressListener listener) {
              this.responseBody = responseBody;
              this.listener = listener;
          }

          @Override
          public MediaType contentType() {
              return responseBody.contentType();
          }

          @Override
          public long contentLength() {
              return responseBody.contentLength();
          }

          @Override
          public BufferedSource source() {
              if (bufferedSource == null) {
                  bufferedSource = Okio.buffer(new ProgressSource(responseBody.source(), contentLength(), listener));
              }
              return bufferedSource;
          }

          private static class ProgressSource extends ForwardingSource {
              private final ProgressListener listener;
              private final long totalBytes;
              private long bytesRead = 0;

              public ProgressSource(Source source, long totalBytes, ProgressListener listener) {
                  super(source);
                  this.totalBytes = totalBytes;
                  this.listener = listener;
              }

              @Override
              public long read(Buffer sink, long byteCount) throws IOException {
                  long bytesReadThisTime = super.read(sink, byteCount);
                  if (bytesReadThisTime != -1) {
                      bytesRead += bytesReadThisTime;
                      if (totalBytes > 0) {
                          int progress = (int) (bytesRead * 100 / totalBytes);
                          listener.onProgress(progress);
                      }
                  }
                  return bytesReadThisTime;
              }
          }
      }
  }
  • 使用示例
  new Thread(() -> {
      String fileUrl = "https://example.com/video.mp4";
      String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/video.mp4";
      String result = OkHttpDownloader.downloadFileWithProgress(fileUrl, savePath, progress -> {
          runOnUiThread(() -> {
              Log.d("Download", "Progress: " + progress + "%");
              // 更新 ProgressBar
          });
      });
      runOnUiThread(() -> {
          Log.d("Download", result);
      });
  }).start();
  • 说明
  • 使用 Interceptor 包装 ResponseBody,监听读取进度。
  • 适合大文件下载,实时更新 UI。

3. 断点续传

通过 Range 请求头实现断点续传。

  • 代码示例
  public class OkHttpDownloader {
      public static String downloadFileWithResume(String fileUrl, String savePath, ProgressListener listener) {
          File file = new File(savePath);
          long downloadedSize = file.exists() ? file.length() : 0;

          OkHttpClient client = new OkHttpClient.Builder()
                  .addNetworkInterceptor(chain -> {
                      Response originalResponse = chain.proceed(chain.request());
                      return originalResponse.newBuilder()
                              .body(new ProgressResponseBody(originalResponse.body(), listener))
                              .build();
                  })
                  .build();

          Request request = new Request.Builder()
                  .url(fileUrl)
                  .get()
                  .addHeader("Range", "bytes=" + downloadedSize + "-")
                  .build();

          try (Response response = client.newCall(request).execute()) {
              if ((response.isSuccessful() || response.code() == 206) && response.body() != null) {
                  if (!file.getParentFile().exists()) {
                      file.getParentFile().mkdirs();
                  }
                  BufferedSink sink = Okio.buffer(Okio.appendingSink(file));
                  sink.writeAll(response.body().source());
                  sink.close();
                  return "Download completed: " + savePath;
              } else {
                  return "Error: HTTP " + response.code();
              }
          } catch (Exception e) {
              e.printStackTrace();
              return "Exception: " + e.getMessage();
          }
      }
  }
  • 使用示例
  new Thread(() -> {
      String fileUrl = "https://example.com/video.mp4";
      String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/video.mp4";
      String result = OkHttpDownloader.downloadFileWithResume(fileUrl, savePath, progress -> {
          runOnUiThread(() -> {
              Log.d("Download", "Progress: " + progress + "%");
          });
      });
      runOnUiThread(() -> {
          Log.d("Download", result);
      });
  }).start();
  • 说明
  • 使用 Range 头指定下载起始位置。
  • 使用 Okio.appendingSink 支持追加写入。
  • 服务器需支持 206 Partial Content

四、使用 Retrofit 实现文件下载

Retrofit 基于 OkHttp,适合 RESTful API 下载,简化代码。

1. 基本文件下载

  • 定义接口
  import okhttp3.ResponseBody;
  import retrofit2.Call;
  import retrofit2.http.GET;
  import retrofit2.http.Streaming;
  import retrofit2.http.Url;

  public interface DownloadService {
      @Streaming // 防止大文件内存溢出
      @GET
      Call<ResponseBody> downloadFile(@Url String fileUrl);
  }
  • 下载代码
  import okhttp3.ResponseBody;
  import retrofit2.Retrofit;
  import retrofit2.Call;
  import okio.BufferedSink;
  import okio.Okio;
  import java.io.File;

  public class RetrofitDownloader {
      public static String downloadFile(String baseUrl, String fileUrl, String savePath) {
          Retrofit retrofit = new Retrofit.Builder()
                  .baseUrl(baseUrl)
                  .build();
          DownloadService service = retrofit.create(DownloadService.class);

          try {
              Call<ResponseBody> call = service.downloadFile(fileUrl);
              Response<ResponseBody> response = call.execute();
              if (response.isSuccessful() && response.body() != null) {
                  File file = new File(savePath);
                  if (!file.getParentFile().exists()) {
                      file.getParentFile().mkdirs();
                  }
                  BufferedSink sink = Okio.buffer(Okio.sink(file));
                  sink.writeAll(response.body().source());
                  sink.close();
                  return "Download completed: " + savePath;
              } else {
                  return "Error: HTTP " + response.code();
              }
          } catch (Exception e) {
              e.printStackTrace();
              return "Exception: " + e.getMessage();
          }
      }
  }
  • 使用示例(Kotlin 协程):
  interface DownloadService {
      @Streaming
      @GET
      suspend fun downloadFile(@Url fileUrl: String): Response<ResponseBody>
  }

  suspend fun downloadFile(baseUrl: String, fileUrl: String, savePath: String): String = withContext(Dispatchers.IO) {
      val retrofit = Retrofit.Builder()
          .baseUrl(baseUrl)
          .build()
      val service = retrofit.create(DownloadService::class.java)

      try {
          val response = service.downloadFile(fileUrl)
          if (response.isSuccessful && response.body() != null) {
              val file = File(savePath)
              if (!file.parentFile.exists()) {
                  file.parentFile.mkdirs()
              }
              val sink = Okio.buffer(Okio.sink(file))
              sink.writeAll(response.body()!!.source())
              sink.close()
              "Download completed: $savePath"
          } else {
              "Error: HTTP ${response.code()}"
          }
      } catch (e: Exception) {
          "Exception: ${e.message}"
      }
  }

  // 调用
  lifecycleScope.launch {
      val result = downloadFile(
          "https://example.com/",
          "https://example.com/image.jpg",
          "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}/image.jpg"
      )
      Log.d("Download", result)
  }
  • 说明
  • 使用 @Streaming 注解避免大文件内存溢出。
  • 结合协程简化异步处理。
  • 依赖 OkHttp 处理底层流。

2. 进度监听

需要结合 OkHttp 的 Interceptor,如 OkHttp 示例中的 ProgressResponseBody

3. 断点续传

通过 OkHttp 的 Range 头实现,Retrofit 需自定义 OkHttpClient:

OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(chain -> {
            Request request = chain.request();
            if (downloadedSize > 0) {
                request = request.newBuilder()
                        .header("Range", "bytes=" + downloadedSize + "-")
                        .build();
            }
            return chain.proceed(request);
        })
        .build();
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(baseUrl)
        .client(client)
        .build();

五、DownloadManager 高级用法

DownloadManager 适合简单下载,支持通知和基本配置。

  • 代码示例(带通知和错误处理):
  import android.app.DownloadManager;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.net.Uri;
  import android.os.Environment;

  public class DownloadManagerUtil {
      public static void downloadFile(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);
          long downloadId = downloadManager.enqueue(request);

          // 注册广播监听下载完成
          context.registerReceiver(new BroadcastReceiver() {
              @Override
              public void onReceive(Context context, Intent intent) {
                  long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
                  if (id == downloadId) {
                      Log.d("Download", "Download completed: " + fileName);
                  }
              }
          }, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
      }
  }
  • 使用示例
  DownloadManagerUtil.downloadFile(this, "https://example.com/image.jpg", "image.jpg");
  • 说明
  • 自动显示下载通知。
  • 通过广播监听下载完成。
  • 不支持进度监听或断点续传。

六、注意事项

  1. 异步处理
  • 下载操作必须在子线程,OkHttp 和 Retrofit 支持异步回调或协程。
  1. 权限管理
  • Android 6.0+:动态请求存储权限。
  • 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); OutputStream out = context.getContentResolver().openOutputStream(uri);
  1. 错误处理
  • 处理 IOException 和 HTTP 错误(4xx、5xx)。
  • 检查 Content-Length 是否有效。
  1. 资源释放
  • OkHttp 自动管理连接,Retrofit 需关闭流。
  • DownloadManager 无需手动释放。
  1. 性能优化
  • 使用缓冲区(4KB 或 8KB)读取流。
  • 支持断点续传,减少重复下载。
  1. 安全性
  • 使用 HTTPS,验证服务器证书。
  • 添加认证头(如 Authorization: Bearer token)。

七、学习建议与实践

  1. 学习路径
  • 掌握 OkHttp 的基本下载和进度监听。
  • 学习 Retrofit 的 @Streaming 注解和异步下载。
  • 实现断点续传,使用 Range 头。
  • 探索 DownloadManager 的简单场景。
  1. 实践项目
  • 简单项目:下载图片并显示。
  • 进阶项目:下载视频,显示进度条。
  • 高级项目:实现断点续传,保存下载状态。
  1. 调试工具
  • Postman:测试下载 URL。
  • Charles/Fiddler:抓包分析。
  • Android Studio Network Profiler:查看请求详情。
  1. 推荐资源
  • OkHttp 文档:https://square.github.io/okhttp/
  • Retrofit 文档:https://square.github.io/retrofit/
  • Android 官方文档:https://developer.android.com/reference/android/app/DownloadManager

八、总结

  • OkHttp:支持异步、进度监听、断点续传,适合复杂下载。
  • Retrofit:简化 RESTful 下载,结合协程优雅高效。
  • DownloadManager:适合简单下载,自动通知。
  • 注意事项:异步处理、权限管理、错误处理、资源释放。
  • 推荐:优先使用 OkHttp 或 Retrofit,DownloadManager 用于简单任务。

如果需要更详细的代码示例(如多文件下载、MediaStore 集成)或特定场景讲解,请告诉我!

类似文章

发表回复

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