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

Android Studio中如何通过多个URL下载Zip文件

Android端多URL批量下载Zip文件落地方案

前置配置

  • 权限适配
    在AndroidManifest.xml中添加必要权限,按系统版本做差异化配置:
    <!-- 基础网络权限,所有Android版本都必须添加 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- Android 12及以下版本,若要写入公共存储目录需要添加,高版本无需配置 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
    
    额外注意:Android 9及以上默认禁止明文HTTP请求,如果你的下载地址是HTTP而非HTTPS,需要在<application>标签下添加android:usesCleartextTraffic="true"配置,否则会被系统直接拦截请求。
  • 技术选型
    优先选OkHttp做网络请求组件,相比原生HttpURLConnection封装更完善,自带超时控制、连接池复用,稳定性更高,只需要在模块级的build.gradle中添加依赖即可:
    dependencies {
        implementation("com.squareup.okhttp3:okhttp:4.12.0")
    }
    
    如果不想引入第三方依赖,直接用原生HttpURLConnection也可实现,只是需要自行补全更多异常处理、流关闭逻辑。如果不需要自定义进度回调、请求头,也可以直接用系统自带的DownloadManager,无需自己处理线程调度,但是批量控制灵活度较低。

核心实现逻辑

  • 前期预处理
    先把Postman中调试通过的URL整理成字符串集合,提前做合法性校验,过滤空值、格式不合法的地址;如果Postman中配置了鉴权token、特殊User-Agent等请求头,要统一记录下来,构造请求时全部带上,避免被服务端拦截返回401/403。
  • 下载调度控制
    不要为每个URL单独开子线程下载,容易占满带宽、触发内存抖动,用固定大小线程池控制并发数即可,常规场景设置2-3个并发下载任务体验最优;Kotlin环境可以直接用协程Dispatchers.IO.limitedParallelism(2)实现更轻量的并发控制。
  • 单文件下载规则
    • 每个下载任务对应独立的本地存储路径,建议用URL的hash值作为Zip文件名,避免文件名冲突
    • 配置合理超时:连接超时10s,读写超时30s即可,百兆以上大文件可以适当调长读写超时时间
    • 读写文件时用8k大小的缓冲区边读边写,禁止把整个Zip文件一次性读入内存,否则大文件场景会直接触发OOM
    • 下载完成后校验本地文件长度和接口返回的content-length是否一致,提前过滤损坏的安装包
    • 所有进度、成功、失败的回调都要切到主线程分发,禁止在子线程直接操作UI控件

如果要下载的Zip单文件超过100M,建议补充断点续传逻辑,用户切网络、退后台之后不需要从头重新下载,体验会好很多。

可直接复用的核心实现代码(Kotlin)

// 全局复用OkHttpClient实例,不要每次下载都新建
private val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .build()

/**
 * 批量下载Zip文件
 * @param urlList 待下载的Zip地址列表
 * @param saveDir 本地存储目录,建议优先传应用私有目录,不需要额外存储权限
 * @param onEachProgress 单个文件下载进度回调,返回0-100的进度值
 * @param onEachSuccess 单个文件下载成功回调,返回对应本地Zip文件
 * @param onEachFailed 单个文件下载失败回调,返回异常信息
 * @param onAllComplete 所有任务(包含成功、失败)执行完成回调
 */
fun batchDownloadZip(
    urlList: List<String>,
    saveDir: File,
    onEachProgress: (url: String, progress: Int) -> Unit,
    onEachSuccess: (url: String, zipFile: File) -> Unit,
    onEachFailed: (url: String, e: Exception) -> Unit,
    onAllComplete: () -> Unit
) {
    if (!saveDir.exists()) saveDir.mkdirs()
    val taskCount = urlList.size
    val countDownLatch = CountDownLatch(taskCount)
    // 固定2个并发的线程池
    val downloadExecutor = Executors.newFixedThreadPool(2)
    val mainHandler = Handler(Looper.getMainLooper())

    urlList.forEach { url ->
        downloadExecutor.execute {
            var inputStream: InputStream? = null
            var fos: FileOutputStream? = null
            try {
                // 如果有自定义请求头,在这里addHeader即可
                val request = Request.Builder().url(url).get().build()
                val response = okHttpClient.newCall(request).execute()
                if (!response.isSuccessful) throw IOException("请求失败,状态码:${response.code}")
                val responseBody = response.body ?: throw IOException("响应体为空")
                val totalSize = responseBody.contentLength()
                // 用url的hash命名,避免重名覆盖
                val targetZipFile = File(saveDir, "${url.hashCode()}.zip")
                inputStream = responseBody.byteStream()
                fos = FileOutputStream(targetZipFile)
                val buffer = ByteArray(8 * 1024) // 8k缓冲区
                var downloadedSize = 0L
                var readLen: Int
                while (inputStream.read(buffer).also { readLen = it } != -1) {
                    fos.write(buffer, 0, readLen)
                    downloadedSize += readLen
                    val progress = if (totalSize > 0) (downloadedSize * 100 / totalSize).toInt() else -1
                    mainHandler.post { onEachProgress(url, progress) }
                }
                fos.flush()
                mainHandler.post { onEachSuccess(url, targetZipFile) }
            } catch (e: Exception) {
                mainHandler.post { onEachFailed(url, e) }
            } finally {
                inputStream?.close()
                fos?.close()
                countDownLatch.countDown()
            }
        }
    }

    // 等待所有任务结束后回调完成
    thread {
        countDownLatch.await()
        downloadExecutor.shutdown()
        mainHandler.post { onAllComplete() }
    }
}

常见坑点规避

  • 绝对禁止在主线程执行网络请求,会直接触发NetworkOnMainThreadException导致崩溃
  • 分区存储适配:优先把文件存在应用私有目录(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)),不需要任何存储权限,适配成本最低;如果要存到系统公共下载目录,不要直接操作公共路径,需要通过MediaStore API或者系统DownloadManager写入,否则高版本系统会报权限错误
  • 下载完成后如果需要解压Zip包,用ZipInputStream流式解压即可,不要把整个压缩包加载到内存,同时提前校验Zip文件头合法性,避免损坏文件导致解压崩溃
  • 下载前先判断网络状态,移动网络场景建议给用户弹流量消耗提示,WiFi场景可以适当调高并发数提升下载速度

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 04:30:50