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
相关产品推荐
相关产品推荐

