Android 文件下载实践:基于 OkHttp 的完整实现与思考

一. 引言

文件下载几乎是每一个稍微复杂一点的 App 都绕不开的需求:下载资源包、音视频文件、离线数据、更新包......可以说是"老朋友"了。

在日常开发中,我们通常都会使用一套成熟的网络请求方案,比如:

  • OkHttpClient:底层网络通信
  • Retrofit:接口层封装
  • Moshi / Gson:JSON 序列化与反序列化

这些框架对接口请求的支持已经非常完善了,写起来也非常优雅。

但是当我想下载数据时比如zip,或者是.mp3,.mp4,当前接口请求这一套就有点满足不了了。

二. 网络请求方案简单回顾

在谈到下载之前,我们先简单回顾一下这网络三套各自的职责。

OkHttpClient

  • 底层 HTTP 客户端
  • 负责:连接、请求、响应、流读取
  • 非常灵活,但偏底层

下载本质上就是 OkHttp 最擅长、但又最原始的事情

Retrofit

  • 基于 OkHttp 的接口封装层
  • 用注解描述 API
  • 非常适合:请求 JSON → 转 Model

但对于下载来说:

  • Retrofit 仍然是 流式下载
  • 进度监听、文件写入,最终还是要你自己处理

Moshi

  • JSON ↔ Model 转换
  • 与下载本身关系不大

那么这么分析完之后,我们就可以看出下载这一套流程和网络请求需要分开来写了。

我们都希望 OkHttp 可以提供一个简单的下载方法,给我们回调进度,给我们下载后的资源地址,但是很遗憾并没有。

OkHttp对下载的态度是 :我只负责网络通讯,至于你要怎么处理数据,那是开发者的事儿。

所以在下载场景中,OkHttp 只给我们:

  • ResponseBody
  • 一个 InputStream

剩下的全交给开发者:

  • 下载进度怎么算
  • 文件存哪
  • 写文件的方式
  • 是否支持协程
  • 异常如何处理

三. 基于 OkHttp 的下载实现

既然需要我们自己来实现细节,那么在写代码之前我们首先要明确,一个完整的下载,至少需要处理哪些事情。

  1. 发起网络请求
  2. 校验 HTTP 状态码
  3. 获取文件总大小(用于进度)
  4. 从 InputStream 读取数据
  5. 按 buffer 写入本地文件
  6. 计算并回调下载进度
  7. 处理异常
  8. 切换到 IO 线程执行

接下来,我们开始围绕上面的这些点开始一步步的展开。

3.1 代码实现

下面是一个简单但是很完整的下载工具类:DownloadHelper

Kotlin 复制代码
package com.example.americandramaassistantandroid.network

import okhttp3.OkHttpClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
import java.io.File
import java.io.IOException

class DownloadHelper {

    private val client = OkHttpClient()

    /// 下载
    suspend fun downloadZip(
        url: String,
        targetFile: File,
        onProgress: (Int) -> Unit
    ) = withContext(Dispatchers.IO) {

        val request = Request.Builder()
            .url(url)
            .build()

        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                throw IOException("Download failed: ${response.code}")
            }

            val body = response.body
                ?: throw IOException("Response body is null")

            val contentLength = body.contentLength()
            val inputStream = body.byteStream()

            var totalRead = 0L
            val buffer = ByteArray(8 * 1024)

            targetFile.outputStream().use { output ->
                while (true) {
                    val read = inputStream.read(buffer)
                    if (read == -1) break

                    output.write(buffer, 0, read)
                    totalRead += read

                    if (contentLength > 0) {
                        val progress = (totalRead * 100 / contentLength).toInt()
                        onProgress(progress)
                    }
                }
                output.flush()
            }
        }
    }
}

3.2 代码解析

使用协程切换到 IO 线程

Kotlin 复制代码
withContext(Dispatchers.IO)
  • 下载和文件写入都属于 IO 操作
  • 避免阻塞主线程
  • 与 ViewModel 非常好配合

构建并执行请求

Kotlin 复制代码
val request = Request.Builder()
    .url(url)
    .build()

client.newCall(request).execute()
  • 这里使用的是 同步请求
  • 但因为在 IO 线程中执行,不会卡 UI
  • 同步请求更利于控制下载流程

校验响应合法性

Kotlin 复制代码
if (!response.isSuccessful) {
    throw IOException("Download failed: ${response.code}")
}
  • 下载失败往往不是"读不到流"
  • 而是 HTTP 状态码就已经失败了

获取文件大小 & 输入流

Kotlin 复制代码
val contentLength = body.contentLength()
val inputStream = body.byteStream()
  • contentLength 用来计算进度
  • 某些服务器可能返回 -1,这也是为什么要做判断

边读边写文件

Kotlin 复制代码
val buffer = ByteArray(8 * 1024)

while (true) {
    val read = inputStream.read(buffer)
    if (read == -1) break

    output.write(buffer, 0, read)
    totalRead += read
}

这是下载的核心逻辑

  • 8KB buffer 是一个常见、合理的大小
  • 一边从网络读
  • 一边往文件写
  • 不占用大量内存

下载进度回调

Kotlin 复制代码
val progress = (totalRead * 100 / contentLength).toInt()
onProgress(progress)
  • 进度完全由我们自己控制
  • 可以很容易和 UI 层绑定
  • 比 Retrofit 的下载监听更直观

四. 使用

在 ViewModel 中使用非常自然:

Kotlin 复制代码
viewModelScope.launch {
    downloadHelper.downloadZip(
        url = downloadUrl,
        targetFile = targetFile
    ) { progress ->
        _downloadProgress.value = progress
    }
}

我们只需要传入文件的下载地址,和文件的保存路径。

而UI层就只需要关心:当前进度、是否完成、是否失败。

五. 结语

下载看起来是一个"老生常谈"的功能,但真正自己实现一次,你会发现:

  • 它非常底层
  • 但也非常可控
  • 非常适合用来理解 OkHttp 的本质

OkHttp 并没有替你"把事做完",而是把选择权全部交给了你。

这既是负担,也是自由。

不过目前就只是个最简单的下载器,如果进一步扩展的话,可以添加下载任务的取消与状态管理,或者支持断点续传的下载实现等等。

也欢迎大家一起讨论关于安卓中的下载。

相关推荐
不急不躁1231 小时前
Android16 GTS GtsPermissionTestcases 测试,跳过权限检查
android
符哥20083 小时前
关于用Android Compose开发成不成熟的分析
android·android jetpack
蜗牛、Z3 小时前
Android 蓝牙/Wi-Fi通信协议之:蓝牙扫描ScanCallback详解
android
黄昏晓x3 小时前
Linux----进程控制
android·linux·运维
我是阿亮啊3 小时前
android中事件分发机制
android·事件分发·事件分发机制
心前阳光4 小时前
Unity 模拟父子关系
android·unity·游戏引擎
2501_915106324 小时前
当 Perfdog 开始收费之后,我重新整理了一替代方案
android·ios·小程序·https·uni-app·iphone·webview
多多*4 小时前
2月3日面试题整理 字节跳动后端开发相关
android·java·开发语言·网络·jvm·adb·c#
习惯就好zz6 小时前
[Android/Linux] 实战记录:利用 Kconfig 精确控制 i.MX8MM 特定 DTB 的编译生成
android·linux·dts·dtb·lunch·多卡板配置
踏雪羽翼6 小时前
android 解决混淆导致AGPBI: {“kind“:“error“,“text“:“Type a.a is defined multiple times
android·java·开发语言·混淆·混淆打包出现a.a