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 并没有替你"把事做完",而是把选择权全部交给了你。

这既是负担,也是自由。

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

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

相关推荐
_李小白2 小时前
【Android 美颜相机】第四天:CameraLoader、Camera1Loader 与 Camera2Loader
android·数码相机
00后程序员张2 小时前
iOS APP 性能测试工具,监控CPU,实时日志输出
android·ios·小程序·https·uni-app·iphone·webview
invicinble2 小时前
认识es的多个维度
android·大数据·elasticsearch
前端切图仔0012 小时前
Chrome 扩展程序上架指南
android·java·javascript·google
黄林晴2 小时前
Compose Multiplatform 1.10.0 重磅发布!三大核心升级,跨平台开发效率再提升
android·android jetpack
锁我喉是吧2 小时前
Android studio 编译faiss
android·android studio·faiss
鹏程十八少3 小时前
3. Android 腾讯开源的 Shadow,凭什么成为插件化“终极方案”?
android·前端·面试
TheNextByte13 小时前
如何通过蓝牙将联系人从Android传输到 iPhone
android·cocoa·iphone
Wpa.wk3 小时前
性能测试-性能监控相关命令-基础篇
android·linux·运维·经验分享·测试工具·性能测试·性能监控