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

Android多文件下载队列开发:服务器图片列表点击下载与重复规避方案咨询

实现高效图片下载与去重的方案

核心逻辑设计

要实现需求的核心是本地存在性校验+下载任务防重复+异步非阻塞执行,这三点能保证逻辑高效且整洁:

  • 用图片的唯一标识绑定本地文件,避免误判
  • 维护正在下载的任务集合,防止重复发起请求
  • 异步执行下载操作,避免阻塞UI并提供实时反馈

具体实现步骤

1. 生成唯一本地标识

为每个图片生成唯一的存储键,优先用服务器返回的图片ID;如果没有ID,可对图片URL做MD5哈希处理,确保不同图片不会冲突,也能准确校验是否已下载。

2. 本地存在性检查

点击列表项时先执行检查:

  • 若本地存在对应文件:直接提示“已下载”或打开本地文件
  • 若不存在:发起下载请求

3. 下载任务并发控制

维护一个Set集合记录当前正在下载的图片标识:

  • 标识已存在:说明该图片正在下载,不重复发起请求
  • 标识不存在:加入集合后启动下载,完成/失败后从集合中移除

4. 异步下载与UI反馈

用异步线程执行下载,同时通过回调给用户展示进度(比如进度百分比、状态文字),避免UI卡顿。

示例代码(Android Kotlin)

1. 下载与存储校验工具类

import android.content.Context
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest

class ImageDownloadManager(private val context: Context) {
    // 记录正在下载的图片标识
    private val downloadingSet = mutableSetOf<String>()
    // 存储目录:应用私有外部存储,无需动态权限(Android 10+)
    private val storageDir: File = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: File(context.filesDir, "Pictures")

    init {
        if (!storageDir.exists()) storageDir.mkdirs()
    }

    // 生成URL的MD5哈希作为唯一标识
    private fun getUniqueKey(url: String): String {
        val bytes = MessageDigest.getInstance("MD5").digest(url.toByteArray())
        return bytes.joinToString("") { "%02x".format(it) }
    }

    // 检查图片是否已下载
    fun isImageDownloaded(url: String): Boolean {
        val key = getUniqueKey(url)
        val targetFile = File(storageDir, "$key.jpg")
        return targetFile.exists() && targetFile.length() > 0
    }

    // 执行图片下载
    fun downloadImage(
        url: String,
        onProgress: (Int) -> Unit,
        onComplete: (File?) -> Unit
    ) {
        val key = getUniqueKey(url)
        // 正在下载中,直接返回
        if (downloadingSet.contains(key)) {
            onComplete(null)
            return
        }
        // 已下载,返回本地文件
        if (isImageDownloaded(url)) {
            onComplete(File(storageDir, "$key.jpg"))
            return
        }

        downloadingSet.add(key)
        Thread {
            var connection: HttpURLConnection? = null
            var inputStream: InputStream? = null
            var outputStream: FileOutputStream? = null
            try {
                val targetFile = File(storageDir, "$key.jpg")
                val urlObj = URL(url)
                connection = urlObj.openConnection() as HttpURLConnection
                connection.connectTimeout = 10000
                connection.readTimeout = 10000

                val totalSize = connection.contentLength
                inputStream = connection.inputStream
                outputStream = FileOutputStream(targetFile)

                val buffer = ByteArray(1024)
                var bytesRead: Int
                var downloadedSize = 0
                while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                    outputStream.write(buffer, 0, bytesRead)
                    downloadedSize += bytesRead
                    val progress = ((downloadedSize.toFloat() / totalSize) * 100).toInt()
                    onProgress(progress)
                }
                outputStream.flush()
                onComplete(targetFile)
            } catch (e: Exception) {
                e.printStackTrace()
                onComplete(null)
            } finally {
                connection?.disconnect()
                inputStream?.close()
                outputStream?.close()
                downloadingSet.remove(key)
            }
        }.start()
    }
}

2. 列表项点击事件处理

// 假设使用RecyclerView展示图片列表
class ImageListAdapter(
    private val imageUrls: List<String>,
    private val downloadManager: ImageDownloadManager
) : RecyclerView.Adapter<ImageListAdapter.ViewHolder>() {

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView: ImageView = itemView.findViewById(R.id.image_view)
        val statusTv: TextView = itemView.findViewById(R.id.status_tv)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_image, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val url = imageUrls[position]
        // 用Glide加载网络图片到列表项
        Glide.with(holder.itemView.context).load(url).into(holder.imageView)

        // 更新初始状态
        if (downloadManager.isImageDownloaded(url)) {
            holder.statusTv.text = "已下载"
        } else {
            holder.statusTv.text = "点击下载"
        }

        // 处理点击事件
        holder.itemView.setOnClickListener {
            if (downloadManager.isImageDownloaded(url)) {
                Toast.makeText(holder.itemView.context, "图片已在本地存储", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            holder.statusTv.text = "下载中..."
            downloadManager.downloadImage(
                url,
                onProgress = { progress ->
                    holder.statusTv.post {
                        holder.statusTv.text = "下载中:$progress%"
                    }
                },
                onComplete = { file ->
                    holder.statusTv.post {
                        if (file != null) {
                            holder.statusTv.text = "已下载"
                            Toast.makeText(holder.itemView.context, "下载完成", Toast.LENGTH_SHORT).show()
                        } else {
                            holder.statusTv.text = "下载失败,点击重试"
                        }
                    }
                }
            )
        }
    }

    override fun getItemCount(): Int = imageUrls.size
}

优化建议

  • 更新校验:如果图片可能在服务器更新,可在下载前请求服务器的文件MD5或Last-Modified时间,判断是否需要重新下载
  • 断点续传:下载时记录已下载字节数,下次请求时设置Range请求头,实现断点续传
  • 并发控制:支持批量下载时,限制同时下载的任务数(比如最多3个),避免占用过多网络资源
  • 权限适配:Android 10以下需动态申请存储权限,iOS需在Info.plist中添加相册写入权限描述

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 22:48:35