AWS S3文件下载优化及AmazonS3Client、Retrofit技术问题咨询
我正在开发一个使用com.amazonaws:aws-android-sdk-s3依赖从服务器下载文件的项目,当前下载实现代码如下:
fun downloadFile(context: Context, bucketName: String, fileName: String, file: File, transferListener: TransferListener) { val transferUtility = TransferUtility.builder().s3Client(AmazonS3Client(AnonymousAWSCredentials())) .context(context.applicationContext).build()!! val observer = transferUtility.download(bucketName, fileName, file) observer.setTransferListener(transferListener) }
应用需定期获取新文件(本文以JSON文件为例,支持任意文件类型),目前通过Firebase-RemoteConfig触发检查,若检测到新值则执行下载,该实现方式繁琐冗余,希望弃用RemoteConfig来管理文件更新。
我了解到Retrofit可通过Etag或Last-Modified等机制判断文件是否更新,但未找到AWS S3的对应实现方案;同时发现当前使用的AmazonS3Client构造方法已过时,官方提示需使用AmazonS3Client#AmazonS3Client(AWSCredentials, com.amazonaws.regions.Region),但不清楚参数配置方式。
后续尝试改用Retrofit实现下载,代码如下:
interface FileDownloadService { @GET @Streaming fun downloadFile(@Url fileUrl:String): Call<ResponseBody> } @WorkerThread fun test(context: App, url: String) { Log.d("AppLog", "test") val cacheSizeBytes = 1024L * 1024L * 2L val builder = OkHttpClient().newBuilder() builder.cache(Cache(context.cacheDir, cacheSizeBytes)) val client: OkHttpClient = builder.build() val retrofitBuilder = Retrofit.Builder() retrofitBuilder.baseUrl("https://s3.amazonaws.com").client(client) val retrofit = retrofitBuilder.build() val service: FileDownloadService = retrofit.create(FileDownloadService::class.java) val response = service.downloadFile(url).execute() Log.d("AppLog", "isSuccessful?${response.isSuccessful} ${response.code()} ${response.raw().code} ${response.headers()["Last-Modified"]}") }
但该方案存在以下问题:
- 模拟器上始终返回响应码200(预期为
HttpURLConnection.HTTP_NOT_MODIFIED),且耗时较长(约4秒),仅能正常获取Last-Modified头部信息; - 真机上出现
UnknownHostException: Unable to resolve host "s3.amazonaws.com": No address associated with hostname异常; - 应用已在其他场景使用Retrofit,担心新实例的配置、线程等与现有实例冲突。
- 如何检测AWS S3中的文件是否变更,从而避免重复下载?
- 已过时的AmazonS3Client构造方法应如何替换?
- 是否可以按上述方式使用Retrofit下载AWS S3文件?若可行,当前实现存在哪些问题?
1. 检测AWS S3文件变更的两种方案
方案一:使用S3 SDK的headObject方法
通过headObject获取文件元数据(Etag、Last-Modified),与本地存储的历史值对比,判断是否需要下载:
fun checkFileUpdated(s3Client: AmazonS3, bucketName: String, fileName: String, localLastModified: Long?): Boolean { val headObjectRequest = HeadObjectRequest(bucketName, fileName) val metadata = s3Client.headObject(headObjectRequest) val remoteLastModified = metadata.lastModified.time return localLastModified == null || remoteLastModified > localLastModified }
使用逻辑:
- 首次下载后,将
Last-Modified时间戳或Etag存入SharedPreferences/本地文件; - 定期调用上述方法对比,返回
true时再执行下载。
方案二:添加条件请求头
不管用TransferUtility还是Retrofit,都可以在请求中携带If-Modified-Since或If-None-Match头:
If-Modified-Since:仅文件在指定时间后修改才返回内容,否则返回304;If-None-Match:仅文件Etag与指定值不同才返回内容,否则返回304。
以TransferUtility为例配置请求:
val request = GetObjectRequest(bucketName, fileName) localLastModified?.let { request.setModifiedSinceDate(Date(it)) } val observer = transferUtility.download(request, file)
2. 替换过时的AmazonS3Client构造方法
官方推荐使用AmazonS3ClientBuilder构建客户端,替代过时构造方法:
// 匿名访问场景 val s3Client = AmazonS3ClientBuilder.standard() .withCredentials(AnonymousAWSCredentialsProvider()) .withRegion(Regions.US_EAST_1) // 替换为你的Bucket所在区域,例如Regions.CN_NORTH_1 .build() // 构建TransferUtility val transferUtility = TransferUtility.builder() .s3Client(s3Client) .context(context.applicationContext) .build()
注意:必须指定Bucket所在的正确区域,否则会出现连接错误或性能问题。
3. Retrofit下载S3文件的可行性及问题修复
可以用Retrofit下载S3文件,当前实现的问题及修复方法如下:
问题1:模拟器始终返回200而非304
原因是未添加条件请求头,手动给接口添加If-Modified-Since参数即可:
interface FileDownloadService { @GET @Streaming fun downloadFile( @Url fileUrl: String, @Header("If-Modified-Since") lastModified: String? // 传入本地存储的Last-Modified字符串 ): Call<ResponseBody> }
调用时传入之前保存的Last-Modified值,文件未修改时服务器会返回304。
问题2:真机出现UnknownHostException
- 检查应用是否添加网络权限:在AndroidManifest.xml中添加
<uses-permission android:name="android.permission.INTERNET"/>; - 确认Bucket的Endpoint是否正确:部分AWS区域的S3域名不是
s3.amazonaws.com,比如中国区为s3.cn-north-1.amazonaws.com.cn,需替换baseUrl为对应区域的Endpoint; - 排查真机网络是否正常,能否访问目标域名。
问题3:Retrofit实例冲突
复用项目中已有的OkHttpClient实例,避免创建新的Retrofit实例:
// 假设项目已有全局的okHttpClient实例 val retrofit = Retrofit.Builder() .baseUrl("https://s3.amazonaws.com") // 或对应区域的Endpoint .client(GlobalOkHttpClient.getInstance()) .build()
若需要特殊配置,可基于现有OkHttpClient构建新实例:
val newClient = GlobalOkHttpClient.getInstance().newBuilder() .cache(Cache(context.cacheDir, 2 * 1024 * 1024)) .build()
内容的提问来源于stack exchange,提问作者android developer

