Android如何通过带签名参数的URL下载文件并监听进度
问题描述
我是Android平台新手,正在开发一款从网络下载文件的应用。网上的示例均为下载无参数的URL(如http/https:sample_address/file_to_download.xyz),但我的应用需要下载带AWS签名参数的URL(如http/https:sample_address/file_to_download.xyz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20221025%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221025T093017Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&response-content-disposition=attachment%3B%20filename%3D%2220_2022-24-10.apk%22&X-Amz-Signature=4968bbf4093b5d1fa6f0368ac1cfa445480bc301b132e740a7cf38fe81d9d2b5)。我使用的下载代码无法正常工作,在获取inputStream时抛出java.io.FileNotFoundException异常。我不想使用DownloadManager,因为需要展示下载进度。
错误原因
- URL参数处理错误:你把URL中的查询参数(
?后的部分)拆出来当成HTTP请求头设置,但AWS签名参数是URL的核心组成部分,必须作为查询参数传递,不能放到请求头里。这导致实际请求的是不带签名参数的原始URL,AWS服务器验证签名失败后返回404错误,HttpURLConnection会将4xx/5xx响应转换为FileNotFoundException抛出。 - 请求配置错误:GET请求不需要设置
urlConnection.doOutput = true,该参数仅用于POST/PUT等需要向服务器写入数据的请求,开启后会自动将请求方法转为POST,与你设置的requestMethod = "GET"冲突。
修复方案
- 直接使用完整的带参数URL创建连接,无需拆分参数。
- 移除
urlConnection.doOutput = true配置。 - 添加响应码检查,非200响应时读取错误流排查问题。
- 确保传入的
apkPath是正确URL编码后的字符串(你的示例URL已编码,无需额外处理)。
修正后的代码
private fun downloadUpdate(apkPath: String, versionCode: Int) = serviceScope.launch { try { val apkFileName = "${versionCode}.apk" val appDataRoot = File(AppConstants.EXTERNAL_STORAGE_PATCH) val file = File(appDataRoot, apkFileName) GLog.d(UpdateSoftwareViewModel.TAG, "AppUpdate apkFileName =${apkFileName}") GLog.d(UpdateSoftwareViewModel.TAG, "AppUpdate apkFileName exist =${file.exists()}") var updatedApkExist = file.exists() if (!updatedApkExist) { // 直接使用完整带参数URL创建连接 val url = URL(apkPath) val urlConnection = url.openConnection() as HttpURLConnection urlConnection.requestMethod = "GET" // 移除无效的doOutput配置 // urlConnection.doOutput = true urlConnection.connect() // 检查响应码,处理非成功响应 val responseCode = urlConnection.responseCode if (responseCode != HttpURLConnection.HTTP_OK) { val errorStream = urlConnection.errorStream val errorMsg = errorStream.bufferedReader().use { it.readText() } GLog.e(UpdateSoftwareViewModel.TAG, "Download failed, code: $responseCode, error: $errorMsg") throw IOException("Download failed with code $responseCode") } val fileOutput = FileOutputStream(file) val inputStream = urlConnection.inputStream // 改用Long避免大文件整数溢出 val totalSize: Long = urlConnection.contentLength.toLong() var downloadedSize = 0L // 增大缓冲区提升下载效率 val buffer = ByteArray(4096) var bufferLength: Int while (inputStream.read(buffer).also { bufferLength = it } > 0 && isActive) { fileOutput.write(buffer, 0, bufferLength) downloadedSize += bufferLength // 计算正确的进度百分比(避免整数除法导致的0值) val progress = if (totalSize > 0) (downloadedSize * 100 / totalSize).toInt() else 0 val message = Message() message.what = AppConstants.DOWN_LOAD_PROGRESS message.arg1 = progress downloadHandlerList.forEach { it.sendMessage(message) } } // 关闭所有流资源 fileOutput.close() inputStream.close() updatedApkExist = downloadedSize == totalSize } if (updatedApkExist) { GLog.d(VerifyUserViewModel.TAG, "AppUpdate do install") installApplication(this@UpdateSoftwareService, file.absolutePath) } stopSelf() } catch (e: MalformedURLException) { e.printStackTrace() GLog.d(UpdateSoftwareViewModel.TAG, "MalformedURLException: ${e.message}") } catch (e: IOException) { e.printStackTrace() GLog.d(UpdateSoftwareViewModel.TAG, "IOException: ${e.message}") } }
额外优化点
- 缓冲区从1024字节改为4096字节,提升大文件下载效率。
- 用Long类型存储文件大小和已下载字节数,避免超过2GB文件的整数溢出问题。
- 修复进度计算逻辑,原来的
downloadedSize / totalSize会因整数除法始终返回0,改为百分比计算后能正确展示进度。
内容的提问来源于stack exchange,提问作者Zappy.Mans
相关产品推荐
相关产品推荐

