Android Studio中如何通过多个URL下载Zip文件
Android端多URL批量下载Zip文件落地方案
前置配置
- 权限适配
在AndroidManifest.xml中添加必要权限,按系统版本做差异化配置:
额外注意:Android 9及以上默认禁止明文HTTP请求,如果你的下载地址是HTTP而非HTTPS,需要在<!-- 基础网络权限,所有Android版本都必须添加 --> <uses-permission android:name="android.permission.INTERNET" /> <!-- Android 12及以下版本,若要写入公共存储目录需要添加,高版本无需配置 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" /><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
相关产品推荐
相关产品推荐

