凌峰创科服务平台

Android下载文件时如何高效断点续传?

核心思路

无论使用哪种方法,下载文件的基本流程都遵循以下步骤:

Android下载文件时如何高效断点续传?-图1
(图片来源网络,侵删)
  1. 建立网络连接:通过 HTTP/HTTPS 协议连接到服务器上的文件 URL。
  2. 发起请求:发送一个 GET 请求来获取文件。
  3. 获取输入流:从服务器的响应中获取一个输入流 (InputStream)。
  4. 写入本地存储:创建一个文件输出流 (FileOutputStream),将输入流中的数据分块读取出来,并写入到设备的外部或内部存储中。
  5. 处理进度和结果:在下载过程中更新 UI 进度条,并在下载完成后通知用户。

使用 HttpURLConnection (原生 API,推荐用于学习和小型项目)

这是 Android SDK 自带的网络 API,无需添加第三方依赖,适合理解网络下载的基本原理。

添加网络权限

app/src/main/AndroidManifest.xml 中添加:

<uses-permission android:name="android.permission.INTERNET" />
<!-- 如果需要下载到外部存储,还需要这个权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Android 10 (API 29) 及以上,推荐使用 MANAGE_EXTERNAL_STORAGE,但需要特殊说明 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

注意:从 Android 10 (API 29) 开始,应用默认在沙盒环境中运行,推荐使用 Context.getExternalFilesDir() 目录,这个目录不需要任何权限,如果必须下载到公共目录(如 Download),则需要处理运行时权限。

实现 DownloadTask

我们使用 AsyncTask (虽然已废弃,但为了简单演示) 或 ExecutorService + Handler 来在后台线程执行下载任务,避免阻塞 UI 线程,在现代 Android 开发中,更推荐使用 Kotlin CoroutinesRxJava

Android下载文件时如何高效断点续传?-图2
(图片来源网络,侵删)

这里我们使用 ExecutorServiceHandler 的方式。

// 在 Activity 或 Fragment 中
private val executor = Executors.newSingleThreadExecutor()
private val handler = Handler(Looper.getMainLooper())
fun downloadFile(urlString: String, destinationPath: File) {
    executor.execute {
        var inputStream: InputStream? = null
        var outputStream: FileOutputStream? = null
        var connection: HttpURLConnection? = null
        try {
            // 1. 创建 URL 和连接
            val url = URL(urlString)
            connection = url.openConnection() as HttpURLConnection
            connection.connect()
            // 2. 检查响应码
            if (connection.responseCode != HttpURLConnection.HTTP_OK) {
                throw IOException("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
            }
            // 3. 获取输入流和文件总大小
            val fileLength = connection.contentLengthLong
            inputStream = connection.inputStream
            // 4. 创建输出流
            outputStream = FileOutputStream(destinationPath)
            // 5. 缓冲区读写
            val buffer = ByteArray(4096) // 4KB 缓冲区
            var bytesRead: Int
            var totalBytesRead = 0L
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                outputStream.write(buffer, 0, bytesRead)
                totalBytesRead += bytesRead
                // 6. 计算并发布进度
                val progress = ((totalBytesRead * 100) / fileLength).toInt()
                handler.post {
                    // 更新 UI,ProgressBar
                    progressBar.progress = progress
                    textViewProgress.text = "$progress%"
                }
            }
            // 7. 下载完成
            handler.post {
                Toast.makeText(this, "下载完成!", Toast.LENGTH_SHORT).show()
            }
        } catch (e: Exception) {
            e.printStackTrace()
            handler.post {
                Toast.makeText(this, "下载失败: ${e.message}", Toast.LENGTH_LONG).show()
            }
        } finally {
            // 8. 关闭流和连接
            inputStream?.close()
            outputStream?.close()
            connection?.disconnect()
        }
    }
}

使用第三方库 (强烈推荐)

手动管理网络连接、线程、进度等非常繁琐,容易出错,在实际项目中,强烈建议使用成熟的第三方库。

OkHttp + Retrofit (业界标准)

OkHttp 是一个高效的 HTTP 客户端,Retrofit 是一个类型安全的 HTTP 客户端,它们是 Android 开发的黄金搭档。

步骤:

Android下载文件时如何高效断点续传?-图3
(图片来源网络,侵删)

a. 添加依赖

// build.gradle (Module :app)
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // 用于解析 JSON
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.okio:okio:2.8.0'

b. 定义下载服务接口

import retrofit2.http.GET
import retrofit2.http.Streaming
import okhttp3.ResponseBody
import retrofit2.Call
interface DownloadApiService {
    // @Streaming 表示直接以流的形式返回,避免内存溢出
    @Streaming
    @GET("path/to/your/file.zip")
    fun downloadFile(): Call<ResponseBody>
}

c. 创建 Retrofit 实例

private val retrofit = Retrofit.Builder()
    .baseUrl("https://your-api-base-url.com/")
    .build()
private val apiService = retrofit.create(DownloadApiService::class.java)

d. 执行下载

fun downloadFileWithRetrofit() {
    val call = apiService.downloadFile()
    call.enqueue(object : Callback<ResponseBody> {
        override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
            if (response.isSuccessful) {
                val body = response.body()
                if (body != null) {
                    saveFile(body)
                }
            } else {
                Toast.makeText(this, "下载失败: ${response.message()}", Toast.LENGTH_SHORT).show()
            }
        }
        override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
            Toast.makeText(this, "下载失败: ${t.message}", Toast.LENGTH_SHORT).show()
        }
    })
}
private fun saveFile(body: ResponseBody) {
    val filePath = File(getExternalFilesDir(null), "downloaded_file.zip")
    try {
        inputStream = body.byteStream()
        outputStream = FileOutputStream(filePath)
        val buffer = ByteArray(4096)
        var bytesRead: Int
        var fileSize = body.contentLength()
        var totalBytesRead = 0L
        while (inputStream.read(buffer).also { bytesRead = it } != -1) {
            outputStream.write(buffer, 0, bytesRead)
            totalBytesRead += bytesRead
            val progress = ((totalBytesRead * 100) / fileSize).toInt()
            // 更新 UI
        }
        Toast.makeText(this, "文件保存到: ${filePath.absolutePath}", Toast.LENGTH_LONG).show()
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        inputStream?.close()
        outputStream?.close()
    }
}

优点

  • 代码简洁,类型安全。
  • 内置了线程管理,无需手动创建线程。
  • @Streaming 注解对于大文件下载至关重要,可以防止将整个文件加载到内存中。
  • 功能强大,易于扩展(如添加拦截器、Gzip 解压等)。

Android DownloadManager (系统服务,适用于简单下载)

DownloadManager 是一个系统级的服务,专门用于处理下载任务,它非常适合下载 APK、MP4 等文件,因为它会处理重试、网络状态变化、通知栏显示等复杂逻辑。

a. 添加权限

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<!-- 如果你希望自己的应用能管理下载任务 -->
<uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />

b. 使用 DownloadManager

fun downloadWithDownloadManager() {
    val uri = Uri.parse("https://your-server.com/file.zip")
    val request = DownloadManager.Request(uri)
    // 设置下载时的网络类型
    request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
    // 设置下载路径和文件名
    // 保存到公共下载目录
    val destination = Environment.DIRECTORY_DOWNLOADS + "/my_downloaded_file.zip"
    request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "my_downloaded_file.zip")
    // 显示下载进度在通知栏
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
    // 设置标题
    request.setTitle("文件下载")
    request.setDescription("正在下载文件...")
    // 获取 DownloadManager 服务并启动下载
    val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
    val downloadId = downloadManager.enqueue(request)
    // (可选) 可以通过 BroadcastReceiver 监听下载完成事件
    // registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}

优点

  • 简单易用:几行代码就能启动下载。
  • 系统级管理:自动处理断点续传、网络切换、通知栏显示。
  • 省电:系统可以优化下载时机。

缺点

  • 灵活性差:难以自定义进度条(只能在通知栏显示)。
  • 不适合应用内集成:主要用于让用户下载文件,而不是在应用内直接使用下载的文件。

关键知识点和最佳实践

  1. 在子线程中执行网络操作 Android 的主线程(UI 线程)不允许执行耗时操作,否则会抛出 NetworkOnMainThreadException 异常,必须使用 AsyncTask, Thread, ExecutorService, Kotlin CoroutinesRxJava 等方式在后台线程执行下载。

  2. 处理大文件与内存 (@Streaming) 如果不使用 @Streaming,Retrofit 默认会将 ResponseBody 的内容全部读入内存中的一个 byte[],对于几百 MB 甚至 GB 的大文件,这会导致 OutOfMemoryError@Streaming 保证 ResponseBody 以流的形式返回,你可以按需读取,从而避免内存问题。

  3. 权限处理 (Android 6.0+) 从 Android 6.0 (Marshmallow) 开始,敏感权限需要在运行时动态请求。

    • 写入存储权限:如果目标目录是外部公共存储(如 Downloads),需要动态请求 WRITE_EXTERNAL_STORAGE
    • 作用域存储 (Scoped Storage):Android 10 (API 29) 引入了作用域存储,应用默认只能访问自己的沙盒目录(如 getExternalFilesDir()),访问公共目录需要特殊处理或用户授权。
  4. 断点续传 对于大文件下载,断点续传非常重要,实现方法是在请求头中添加 Range

    • 初始请求HEAD 请求或 GET 请求,检查服务器是否支持 Accept-Ranges: bytes
    • 续传请求:如果本地已有部分文件,获取其大小,然后发送 GET 请求,并设置头信息:Range: bytes=startByte-
    • OkHttp 和 Retrofit 可以方便地自定义请求头。
  5. 使用 WorkManager (推荐的后台任务方案) 如果你的下载任务需要在应用关闭后、设备重启后甚至系统升级后仍然能继续执行,WorkManager 是最佳选择,它是一个用于保证任务可靠执行的库,可以处理网络约束、存储约束等。

总结与选择

方案 优点 缺点 适用场景
HttpURLConnection 无需依赖,轻量级 代码繁琐,需手动处理线程、错误等 学习网络基础,或对依赖有极简要求的小型项目
OkHttp + Retrofit 业界标准,代码优雅,功能强大,类型安全,支持流式下载 需要添加第三方依赖 绝大多数 Android 应用的首选,特别是需要复杂网络交互和自定义下载逻辑时
DownloadManager 系统服务,简单易用,自动处理重试和通知 灵活性差,不适合应用内集成 下载 APK、更新包等,让用户从浏览器或通知栏获取文件的场景

给你的建议:

  • 如果你是初学者:先用 HttpURLConnection 实现一次,理解其原理。
  • 如果你在开发商业应用或任何正式项目直接使用 OkHttp + Retrofit,这是最专业、最高效、最可靠的方式。
  • 如果你只是想让用户下载一个文件,不关心下载过程,也不需要在应用内立即使用它:可以考虑 DownloadManager
分享:
扫描分享到社交APP
上一篇
下一篇