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的适配器 } }
几个关键细节要注意
- 断点续传的正确性:必须在恢复下载时添加
Range请求头,同时文件流要用追加模式,否则会覆盖已下载内容。 - 线程安全:因为多任务可能同时修改任务集合,一定要用
ConcurrentHashMap或者加锁。 - 通知管理:如果下载任务多,可以用Android的折叠通知分组展示,每个任务对应一个子通知,方便用户直接在通知栏操作。
- 任务持久化:如果需要重启App后恢复未完成的任务,可以把任务信息(ID、URL、已下载长度、状态)保存到Room或者SharedPreferences,Service启动时重新加载。
内容的提问来源于stack exchange,提问作者hpAndro
相关产品推荐
相关产品推荐

