You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android多线程下载(启动/暂停/恢复):单任务暂停恢复实现咨询

我之前也做过类似的浏览器风格下载功能,正好能解决你这个单个任务暂停恢复的问题。核心思路是给每个下载任务独立封装状态和控制逻辑,再在Service里统一管理这些任务,下面一步步给你拆解:

1. 先封装独立的下载任务实体

每个下载任务必须是“自给自足”的——自己管自己的状态、进度、暂停/恢复逻辑,这样才能单独操作。我通常会这么设计:

首先定义一个状态枚举:

enum class DownloadState {
    WAITING, DOWNLOADING, PAUSED, COMPLETED, FAILED
}

然后封装DownloadTask类(这里用OkHttp做下载,你也可以换成其他库):

class DownloadTask(
    val taskId: String, // 唯一标识,比如UUID或者URL的哈希值
    val url: String,
    val savePath: String,
    private val onProgressUpdate: (taskId: String, downloaded: Long, total: Long, state: DownloadState) -> Unit
) {
    private var call: Call? = null
    var state = DownloadState.WAITING
        private set
    var downloadedLength = 0L
        private set
    private var totalLength = 0L

    fun start() {
        if (state == DownloadState.DOWNLOADING) return
        state = DownloadState.DOWNLOADING

        val request = Request.Builder()
            .url(url)
            .apply {
                // 断点续传:如果已经下载过一部分,加Range头
                if (downloadedLength > 0) {
                    addHeader("Range", "bytes=$downloadedLength-")
                }
            }
            .build()

        call = OkHttpClient().newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                state = DownloadState.FAILED
                onProgressUpdate(taskId, downloadedLength, totalLength, state)
            }

            override fun onResponse(call: Call, response: Response) {
                response.body?.let { body ->
                    // 获取总长度:如果是续传,总长度是剩余长度+已下载长度
                    totalLength = if (downloadedLength > 0) {
                        downloadedLength + body.contentLength()
                    } else {
                        body.contentLength()
                    }

                    val file = File(savePath)
                    // 追加模式打开文件流,避免覆盖已下载内容
                    val outputStream = FileOutputStream(file, true)
                    val buffer = ByteArray(4096)
                    var bytesRead: Int

                    while (state == DownloadState.DOWNLOADING && body.byteStream().read(buffer).also { bytesRead = it } != -1) {
                        outputStream.write(buffer, 0, bytesRead)
                        downloadedLength += bytesRead
                        onProgressUpdate(taskId, downloadedLength, totalLength, state)
                    }

                    outputStream.close()
                    body.close()

                    // 检查是否是正常完成还是被暂停
                    if (downloadedLength == totalLength) {
                        state = DownloadState.COMPLETED
                    } else if (state == DownloadState.PAUSED) {
                        // 暂停状态无需额外操作
                    } else {
                        state = DownloadState.FAILED
                    }
                    onProgressUpdate(taskId, downloadedLength, totalLength, state)
                } ?: run {
                    state = DownloadState.FAILED
                    onProgressUpdate(taskId, downloadedLength, totalLength, state)
                }
            }
        })
    }

    fun pause() {
        if (state != DownloadState.DOWNLOADING) return
        state = DownloadState.PAUSED
        call?.cancel() // 取消当前请求
        onProgressUpdate(taskId, downloadedLength, totalLength, state)
    }

    fun resume() {
        if (state != DownloadState.PAUSED) return
        start() // 直接调用start,内部会处理续传逻辑
    }

    fun cancel() {
        call?.cancel()
        state = DownloadState.FAILED
        // 可以删除已下载的临时文件
        File(savePath).delete()
        onProgressUpdate(taskId, 0, 0, state)
    }
}
2. 在Foreground Service中管理多任务

用Foreground Service来保证后台下载不被系统杀死,同时维护一个任务集合来管理所有下载任务:

class DownloadService : Service() {
    // 用ConcurrentHashMap保证线程安全
    private val taskMap = ConcurrentHashMap<String, DownloadTask>()
    // 用来给页面同步任务状态的LiveData
    private val taskLiveData = MutableLiveData<List<DownloadInfo>>()
    private val notificationManager by lazy { getSystemService(NotificationManager::class.java) }

    override fun onCreate() {
        super.onCreate()
        // 启动为前台服务,展示下载通知
        startForeground(NOTIFICATION_ID, createNotification())
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        intent?.let {
            when (it.action) {
                ACTION_ADD_TASK -> {
                    val taskId = it.getStringExtra(EXTRA_TASK_ID) ?: return START_STICKY
                    val url = it.getStringExtra(EXTRA_URL) ?: return START_STICKY
                    val savePath = it.getStringExtra(EXTRA_SAVE_PATH) ?: return START_STICKY
                    addTask(taskId, url, savePath)
                }
                ACTION_PAUSE_TASK -> {
                    val taskId = it.getStringExtra(EXTRA_TASK_ID) ?: return START_STICKY
                    taskMap[taskId]?.pause()
                }
                ACTION_RESUME_TASK -> {
                    val taskId = it.getStringExtra(EXTRA_TASK_ID) ?: return START_STICKY
                    taskMap[taskId]?.resume()
                }
                ACTION_CANCEL_TASK -> {
                    val taskId = it.getStringExtra(EXTRA_TASK_ID) ?: return START_STICKY
                    taskMap.remove(taskId)?.cancel()
                    updateLiveData()
                }
            }
        }
        return START_STICKY
    }

    private fun addTask(taskId: String, url: String, savePath: String) {
        if (taskMap.containsKey(taskId)) return
        val task = DownloadTask(taskId, url, savePath) { taskId, downloaded, total, state ->
            // 进度更新时同步LiveData和通知
            updateLiveData()
            updateNotification()
        }
        taskMap[taskId] = task
        task.start()
        updateLiveData()
    }

    private fun updateLiveData() {
        val taskInfoList = taskMap.values.map {
            DownloadInfo(it.taskId, it.url, it.savePath, it.downloadedLength, it.totalLength, it.state)
        }
        taskLiveData.postValue(taskInfoList)
    }

    // 省略通知创建和更新的代码,你可以根据需求做折叠通知或者单个任务通知
    private fun createNotification(): Notification {
        // 实现自己的通知布局
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("下载服务运行中")
            .setSmallIcon(R.drawable.ic_download)
            .build()
    }

    private fun updateNotification() {
        // 更新通知内容,比如显示所有任务的进度
    }

    // 绑定Service,让页面可以获取LiveData
    override fun onBind(intent: Intent): IBinder? {
        return LocalBinder()
    }

    inner class LocalBinder : Binder() {
        fun getService(): DownloadService = this@DownloadService
    }

    companion object {
        const val CHANNEL_ID = "download_channel"
        const val NOTIFICATION_ID = 1001
        const val ACTION_ADD_TASK = "com.example.download.ADD_TASK"
        const val ACTION_PAUSE_TASK = "com.example.download.PAUSE_TASK"
        const val ACTION_RESUME_TASK = "com.example.download.RESUME_TASK"
        const val ACTION_CANCEL_TASK = "com.example.download.CANCEL_TASK"
        const val EXTRA_TASK_ID = "task_id"
        const val EXTRA_URL = "url"
        const val EXTRA_SAVE_PATH = "save_path"
    }
}

// 给页面用的纯数据类,只暴露状态和进度
data class DownloadInfo(
    val taskId: String,
    val url: String,
    val savePath: String,
    val downloadedLength: Long,
    val totalLength: Long,
    val state: DownloadState
)
3. 页面中控制单个任务并展示进度

在下载页面,你可以通过绑定Service来获取taskLiveData,观察任务列表的变化,同时给每个任务的暂停/恢复按钮绑定点击事件:

class DownloadActivity : AppCompatActivity() {
    private lateinit var downloadService: DownloadService
    private var isBound = false

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            val binder = service as DownloadService.LocalBinder
            downloadService = binder.getService()
            isBound = true
            // 观察任务列表变化,更新UI
            downloadService.taskLiveData.observe(this@DownloadActivity) { taskList ->
                // 更新RecyclerView展示所有任务的进度和状态
                updateDownloadList(taskList)
            }
        }

        override fun onServiceDisconnected(arg0: ComponentName) {
            isBound = false
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download)
        // 绑定Service
        Intent(this, DownloadService::class.java).also { intent ->
            bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
        }
    }

    // 比如点击暂停按钮时调用
    fun onPauseTaskClick(taskId: String) {
        if (isBound) {
            // 方式1:直接调用Service的方法
            downloadService.taskMap[taskId]?.pause()
            // 方式2:发送Intent(适合未绑定的场景)
            /*Intent(DownloadService.ACTION_PAUSE_TASK).also {
                it.putExtra(DownloadService.EXTRA_TASK_ID, taskId)
                startService(it)
            }*/
        }
    }

    // 点击恢复按钮时调用
    fun onResumeTaskClick(taskId: String) {
        if (isBound) {
            downloadService.taskMap[taskId]?.resume()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (isBound) {
            unbindService(serviceConnection)
            isBound = false
        }
    }

    private fun updateDownloadList(taskList: List<DownloadInfo>) {
        // 更新RecyclerView的适配器
    }
}
几个关键细节要注意
  1. 断点续传的正确性:必须在恢复下载时添加Range请求头,同时文件流要用追加模式,否则会覆盖已下载内容。
  2. 线程安全:因为多任务可能同时修改任务集合,一定要用ConcurrentHashMap或者加锁。
  3. 通知管理:如果下载任务多,可以用Android的折叠通知分组展示,每个任务对应一个子通知,方便用户直接在通知栏操作。
  4. 任务持久化:如果需要重启App后恢复未完成的任务,可以把任务信息(ID、URL、已下载长度、状态)保存到Room或者SharedPreferences,Service启动时重新加载。

内容的提问来源于stack exchange,提问作者hpAndro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:26:08