Android网络框架封装 ---> Retrofit + OkHttp + 协程 + LiveData + 断点续传 + 多线程下载 + 进度框交互
目录
1. 项目概述
1.1 项目背景
1.2 技术选型
1.3 项目目标
2. 技术架构
2.1 分层架构
2.2 设计原则
3. 核心功能实现
3.1 网络配置管理器
3.2 拦截器体系
3.3 API接口定义
3.4 数据模型
4. 多BaseUrl管理
4.1 依赖注入配置
4.2 Repository层实现
4.3 ViewModel层实现
5. 断点续传实现
5.1 断点续传原理
5.2 断点续传管理器
5.3 文件管理器
6. 多线程下载
6.1 多线程下载原理
6.2 多线程下载管理器
6.3 下载状态管理
7. 进度框交互
7.1 进度框状态管理
7.2 自定义进度对话框
7.3 进度框布局文件
7.4 Activity/Fragment使用示例
8. 错误处理机制
8.1 统一错误处理
8.2 网络状态监听
9. 性能优化
9.1 连接池与缓存优化
9.2 内存优化
9.3 并发控制
10. 最佳实践
10.1 代码组织
10.2 错误处理
10.3 性能优化
10.4 用户体验
10.5 扩展性
11. 扩展功能
11.1 上传功能
11.2 任务队列
11.3 数据库持久化
12. 总结
12.1 技术亮点
12.2 应用场景
12.3 团队协作
12.4 未来扩展
13. 常见问题与答疑
13.1 断点续传相关
13.2 多线程下载相关
13.3 性能优化相关
13.4 用户体验相关
14. 结语
1. 项目概述
1.1 项目背景
随着移动应用功能的不断丰富,网络通信需求日益复杂:
- 多服务器架构(API、CDN、上传、下载服务分离)
- 大文件下载/上传需要断点续传
- 用户体验要求高(进度可视化、操作可控)
- 代码可维护性和扩展性要求
1.2 技术选型
技术 | 版本 | 作用 |
---|---|---|
Retrofit | 2.9.0 | HTTP客户端,API接口定义 |
OkHttp | 4.9.0 | 底层网络库,拦截器支持 |
Kotlin协程 | 1.6.0 | 异步处理,替代回调 |
LiveData | 2.5.0 | 响应式数据流 |
Hilt | 2.44 | 依赖注入 |
Room | 2.5.0 | 本地数据库缓存 |
1.3 项目目标
- 统一网络请求管理
- 支持多BaseUrl动态切换
- 实现断点续传和多线程下载
- 提供友好的进度框交互
- 完善的错误处理和重试机制
2. 技术架构
2.1 分层架构
UI层(Activity/Fragment/Compose)
│
ViewModel层(业务逻辑、状态管理、进度反馈)
│
Repository层(数据获取、缓存、网络请求)
│
Network层(Retrofit+OkHttp+拦截器+多BaseUrl+下载管理)
│
Data层(数据模型、数据库、缓存)
2.2 设计原则
- 单一职责原则:每个类只负责一个功能
- 依赖倒置原则:高层模块不依赖低层模块
- 开闭原则:对扩展开放,对修改关闭
- 接口隔离原则:使用多个专门的接口
3. 核心功能实现
3.1 网络配置管理器
@Singletonclass NetworkManager @Inject constructor(private val context: Context) {private val networkConfigs = mutableMapOf<NetworkType, NetworkConfig>()private val okHttpClients = mutableMapOf<NetworkType, OkHttpClient>()private val retrofitInstances = mutableMapOf<NetworkType, Retrofit>()enum class NetworkType {API_SERVER, // 主API服务器CDN_SERVER, // CDN服务器UPLOAD_SERVER, // 上传服务器DOWNLOAD_SERVER // 下载服务器}data class NetworkConfig(val baseUrl: String,val timeout: Long = 30L,val enableLogging: Boolean = true,val enableAuth: Boolean = true,val customHeaders: Map<String, String> = emptyMap())init {initializeNetworkConfigs()}private fun initializeNetworkConfigs() {networkConfigs[NetworkType.API_SERVER] = NetworkConfig(baseUrl = "https://api.example.com/",timeout = 30L,enableLogging = true,enableAuth = true,customHeaders = mapOf("Accept" to "application/json","User-Agent" to "AndroidApp/1.0"))networkConfigs[NetworkType.CDN_SERVER] = NetworkConfig(baseUrl = "https://cdn.example.com/",timeout = 60L,enableLogging = false,enableAuth = false,customHeaders = mapOf("Cache-Control" to "max-age=3600"))networkConfigs[NetworkType.UPLOAD_SERVER] = NetworkConfig(baseUrl = "https://upload.example.com/",timeout = 120L,enableLogging = true,enableAuth = true,customHeaders = mapOf("Content-Type" to "multipart/form-data"))networkConfigs[NetworkType.DOWNLOAD_SERVER] = NetworkConfig(baseUrl = "https://download.example.com/",timeout = 300L,enableLogging = false,enableAuth = false,customHeaders = mapOf("Accept-Ranges" to "bytes"))}fun getOkHttpClient(type: NetworkType): OkHttpClient {return okHttpClients.getOrPut(type) {createOkHttpClient(type)}}fun getRetrofit(type: NetworkType): Retrofit {return retrofitInstances.getOrPut(type) {createRetrofit(type)}}private fun createOkHttpClient(type: NetworkType): OkHttpClient {val config = networkConfigs[type] ?: throw IllegalArgumentException("Unknown network type: $type")return OkHttpClient.Builder().apply {connectTimeout(config.timeout, TimeUnit.SECONDS)readTimeout(config.timeout, TimeUnit.SECONDS)writeTimeout(config.timeout, TimeUnit.SECONDS)if (config.enableLogging) {addInterceptor(LoggingInterceptor())}if (config.enableAuth) {addInterceptor(AuthInterceptor())}if (config.customHeaders.isNotEmpty()) {addInterceptor(CustomHeadersInterceptor(config.customHeaders))}addInterceptor(RetryInterceptor())addInterceptor(CacheInterceptor(context))}.build()}private fun createRetrofit(type: NetworkType): Retrofit {val config = networkConfigs[type] ?: throw IllegalArgumentException("Unknown network type: $type")return Retrofit.Builder().baseUrl(config.baseUrl).client(getOkHttpClient(type)).addConverterFactory(GsonConverterFactory.create()).build()}}
3.2 拦截器体系
// 日志拦截器class LoggingInterceptor : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request()Log.d("Network", "Request: ${request.url}")val response = chain.proceed(request)Log.d("Network", "Response: ${response.code}")return response}}// 认证拦截器class AuthInterceptor : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val originalRequest = chain.request()val newRequest = originalRequest.newBuilder().addHeader("Authorization", "Bearer $token").addHeader("Content-Type", "application/json").build()return chain.proceed(newRequest)}}// 自定义头部拦截器class CustomHeadersInterceptor(private val headers: Map<String, String>) : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val originalRequest = chain.request()val newRequest = originalRequest.newBuilder().apply {headers.forEach { (key, value) ->addHeader(key, value)}}.build()return chain.proceed(newRequest)}}// 重试拦截器class RetryInterceptor : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request()var response: Response? = nullvar exception: Exception? = nullrepeat(3) { attempt ->try {response = chain.proceed(request)if (response?.isSuccessful == true) {return response!!}} catch (e: Exception) {exception = eif (attempt == 2) throw e}}return response ?: throw exception ?: Exception("Request failed")}}// 缓存拦截器class CacheInterceptor(context: Context) : Interceptor {private val cache = Cache(directory = File(context.cacheDir, "http_cache"),maxSize = 10 * 1024 * 1024 // 10MB)override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request()val response = chain.proceed(request)return response.newBuilder().header("Cache-Control", "public, max-age=3600").build()}}
3.3 API接口定义
// 主API服务interface ApiService {@GET("users")suspend fun getUsers(): Response<List<User>>@POST("users")suspend fun createUser(@Body user: User): Response<User>@GET("posts/{id}")suspend fun getPost(@Path("id") id: Int): Response<Post>}// CDN服务interface CdnService {@GET("images/{imageId}")suspend fun getImage(@Path("imageId") imageId: String): Response<ResponseBody>@GET("videos/{videoId}")suspend fun getVideo(@Path("videoId") videoId: String): Response<ResponseBody>}// 上传服务interface UploadService {@Multipart@POST("upload")suspend fun uploadFile(@Part file: MultipartBody.Part,@Part("description") description: RequestBody): Response<UploadResponse>@Multipart@POST("upload/multiple")suspend fun uploadMultipleFiles(@Part files: List<MultipartBody.Part>): Response<UploadResponse>}// 下载服务interface DownloadService {@Streaming@GETsuspend fun downloadFile(@Url url: String): Response<ResponseBody>@HEADsuspend fun getFileInfo(@Url url: String): Response<Unit>}
3.4 数据模型
// 用户模型data class User(val id: Int,val name: String,val email: String,val avatar: String? = null)// 帖子模型data class Post(val id: Int,val title: String,val content: String,val userId: Int,val createdAt: String)// 上传响应data class UploadResponse(val success: Boolean,val url: String?,val message: String?)// 统一响应格式data class ApiResponse<T>(val code: Int,val message: String,val data: T?)
4. 多BaseUrl管理
4.1 依赖注入配置
@Module@InstallIn(SingletonComponent::class)object NetworkModule {@Provides@Singletonfun provideNetworkManager(@ApplicationContext context: Context): NetworkManager {return NetworkManager(context)}@Provides@Singletonfun provideMultiThreadDownloader(networkManager: NetworkManager,fileManager: FileManager): MultiThreadDownloader {return MultiThreadDownloader(networkManager, fileManager)}@Provides@Singletonfun provideFileManager(@ApplicationContext context: Context): FileManager {return FileManager(context)}// 提供不同类型的API服务@Provides@Singletonfun provideApiService(networkManager: NetworkManager): ApiService {return networkManager.getRetrofit(NetworkType.API_SERVER).create(ApiService::class.java)}@Provides@Singletonfun provideCdnService(networkManager: NetworkManager): CdnService {return networkManager.getRetrofit(NetworkType.CDN_SERVER).create(CdnService::class.java)}@Provides@Singletonfun provideUploadService(networkManager: NetworkManager): UploadService {return networkManager.getRetrofit(NetworkType.UPLOAD_SERVER).create(UploadService::class.java)}@Provides@Singletonfun provideDownloadService(networkManager: NetworkManager): DownloadService {return networkManager.getRetrofit(NetworkType.DOWNLOAD_SERVER).create(DownloadService::class.java)}}
4.2 Repository层实现
class UserRepository @Inject constructor(private val apiService: ApiService,private val userDao: UserDao) {suspend fun getUsers(): Result<List<User>> {return try {// 先尝试从缓存获取val cachedUsers = userDao.getAllUsers()if (cachedUsers.isNotEmpty()) {return Result.success(cachedUsers)}// 从网络获取val response = apiService.getUsers()if (response.isSuccessful) {val users = response.body() ?: emptyList()// 缓存到数据库userDao.insertUsers(users)Result.success(users)} else {Result.failure(Exception("Network error: ${response.code()}"))}} catch (e: Exception) {Result.failure(e)}}suspend fun createUser(user: User): Result<User> {return try {val response = apiService.createUser(user)if (response.isSuccessful) {val createdUser = response.body()!!// 更新本地缓存userDao.insertUser(createdUser)Result.success(createdUser)} else {Result.failure(Exception("Create user failed"))}} catch (e: Exception) {Result.failure(e)}}}
4.3 ViewModel层实现
@HiltViewModelclass UserViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {private val _users = MutableLiveData<List<User>>()val users: LiveData<List<User>> = _usersprivate val _loading = MutableLiveData<Boolean>()val loading: LiveData<Boolean> = _loadingprivate val _error = MutableLiveData<String>()val error: LiveData<String> = _errorfun loadUsers() {viewModelScope.launch {_loading.value = true_error.value = nulluserRepository.getUsers().onSuccess { users ->_users.value = users}.onFailure { exception ->_error.value = exception.message}_loading.value = false}}fun createUser(user: User) {viewModelScope.launch {_loading.value = trueuserRepository.createUser(user).onSuccess { newUser ->// 更新用户列表val currentUsers = _users.value?.toMutableList() ?: mutableListOf()currentUsers.add(newUser)_users.value = currentUsers}.onFailure { exception ->_error.value = exception.message}_loading.value = false}}}
5. 断点续传实现
5.1 断点续传原理
断点续传的核心原理是HTTP的Range请求头:
- 检查服务器支持:通过HEAD请求检查Accept-Ranges: bytes
- 获取文件大小:通过Content-Length头获取文件总大小
- 记录已下载:保存已下载的字节数
- Range请求:使用Range: bytes=已下载字节数-继续下载
- 追加写入:将新下载的内容追加到临时文件
5.2 断点续传管理器
@Singletonclass ResumableDownloader @Inject constructor(private val apiService: ApiService,private val fileManager: FileManager) {// 下载任务状态sealed class DownloadState {object Idle : DownloadState()data class Downloading(val progress: Int, val downloadedBytes: Long, val totalBytes: Long) : DownloadState()data class Paused(val downloadedBytes: Long, val totalBytes: Long) : DownloadState()data class Completed(val file: File) : DownloadState()data class Error(val message: String) : DownloadState()}// 下载任务信息data class DownloadTask(val id: String,val url: String,val fileName: String,val filePath: String,var downloadedBytes: Long = 0,var totalBytes: Long = 0,var state: DownloadState = DownloadState.Idle)// 开始下载suspend fun downloadFile(url: String,fileName: String,onProgress: (Int, Long, Long) -> Unit,onComplete: (File) -> Unit,onError: (String) -> Unit) {try {// 1. 检查服务器是否支持断点续传val rangeSupport = checkRangeSupport(url)if (!rangeSupport) {// 不支持断点续传,使用普通下载downloadWithoutResume(url, fileName, onProgress, onComplete, onError)return}// 2. 获取文件总大小val totalSize = getFileSize(url)// 3. 获取已下载大小val downloadedSize = fileManager.getDownloadedSize(fileName)// 4. 创建临时文件val tempFile = fileManager.createTempFile(fileName)// 5. 执行断点续传下载downloadWithResume(url = url,tempFile = tempFile,downloadedSize = downloadedSize,totalSize = totalSize,onProgress = onProgress,onComplete = { val finalFile = fileManager.renameTempFile("$fileName.tmp", fileName)onComplete(finalFile)},onError = onError)} catch (e: Exception) {onError(e.message ?: "Download failed")}}// 检查服务器是否支持断点续传private suspend fun checkRangeSupport(url: String): Boolean {return try {val response = apiService.head(url)val acceptRanges = response.headers()["Accept-Ranges"]acceptRanges == "bytes"} catch (e: Exception) {false}}// 获取文件大小private suspend fun getFileSize(url: String): Long {val response = apiService.head(url)return response.headers()["Content-Length"]?.toLong() ?: -1L}// 断点续传下载实现private suspend fun downloadWithResume(url: String,tempFile: File,downloadedSize: Long,totalSize: Long,onProgress: (Int, Long, Long) -> Unit,onComplete: () -> Unit,onError: (String) -> Unit) {try {// 创建带Range头的请求val request = Request.Builder().url(url).addHeader("Range", "bytes=$downloadedSize-").build()val response = apiService.downloadWithRange(request)if (!response.isSuccessful) {onError("Download failed: ${response.code}")return}// 获取响应流val inputStream = response.body()?.byteStream()val outputStream = FileOutputStream(tempFile, true) // 追加模式val buffer = ByteArray(8192)var bytesRead: Intvar totalDownloaded = downloadedSizewhile (inputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {outputStream.write(buffer, 0, bytesRead)totalDownloaded += bytesRead// 计算进度val progress = ((totalDownloaded * 100) / totalSize).toInt()onProgress(progress, totalDownloaded, totalSize)}outputStream.close()inputStream?.close()onComplete()} catch (e: Exception) {onError(e.message ?: "Download failed")}}// 普通下载(不支持断点续传)private suspend fun downloadWithoutResume(url: String,fileName: String,onProgress: (Int, Long, Long) -> Unit,onComplete: (File) -> Unit,onError: (String) -> Unit) {try {val response = apiService.download(url)val totalSize = response.body()?.contentLength() ?: -1Lval tempFile = fileManager.createTempFile(fileName)val inputStream = response.body()?.byteStream()val outputStream = FileOutputStream(tempFile)val buffer = ByteArray(8192)var bytesRead: Intvar totalDownloaded = 0Lwhile (inputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {outputStream.write(buffer, 0, bytesRead)totalDownloaded += bytesReadif (totalSize > 0) {val progress = ((totalDownloaded * 100) / totalSize).toInt()onProgress(progress, totalDownloaded, totalSize)}}outputStream.close()inputStream?.close()val finalFile = fileManager.renameTempFile("$fileName.tmp", fileName)onComplete(finalFile)} catch (e: Exception) {onError(e.message ?: "Download failed")}}}
5.3 文件管理器
@Singletonclass FileManager @Inject constructor(private val context: Context) {// 获取下载目录fun getDownloadDirectory(): File {return context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?: File(context.filesDir, "downloads")}// 创建临时文件fun createTempFile(fileName: String): File {val downloadDir = getDownloadDirectory()if (!downloadDir.exists()) {downloadDir.mkdirs()}return File(downloadDir, "$fileName.tmp")}// 检查文件是否存在fun isFileExists(fileName: String): Boolean {val file = File(getDownloadDirectory(), fileName)return file.exists()}// 获取已下载的文件大小fun getDownloadedSize(fileName: String): Long {val tempFile = File(getDownloadDirectory(), "$fileName.tmp")return if (tempFile.exists()) tempFile.length() else 0L}// 重命名临时文件为最终文件fun renameTempFile(tempFileName: String, finalFileName: String): File {val tempFile = File(getDownloadDirectory(), tempFileName)val finalFile = File(getDownloadDirectory(), finalFileName)tempFile.renameTo(finalFile)return finalFile}// 删除文件fun deleteFile(fileName: String): Boolean {val file = File(getDownloadDirectory(), fileName)return file.delete()}// 获取文件大小fun getFileSize(fileName: String): Long {val file = File(getDownloadDirectory(), fileName)return if (file.exists()) file.length() else 0L}// 格式化文件大小fun formatFileSize(bytes: Long): String {return when {bytes < 1024 -> "$bytes B"bytes < 1024 * 1024 -> "${bytes / 1024} KB"bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"else -> "${bytes / (1024 * 1024 * 1024)} GB"}}}
6. 多线程下载
6.1 多线程下载原理
多线程下载的核心思想是将大文件分割成多个小块,然后同时下载这些小块:
- 获取文件大小:通过HEAD请求获取文件总大小
- 分片计算:根据线程数计算每个分片的大小和范围
- 并发下载:多个协程同时下载不同的分片
- 进度统计:实时统计所有分片的下载进度
- 文件合并:所有分片下载完成后合并成完整文件
6.2 多线程下载管理器
@Singletonclass MultiThreadDownloader @Inject constructor(private val networkManager: NetworkManager,private val fileManager: FileManager) {private val downloadJobs = mutableMapOf<String, Job>()private val downloadTasks = mutableMapOf<String, DownloadTask>()// 下载分片信息data class DownloadChunk(val startByte: Long,val endByte: Long,val index: Int,var downloadedBytes: Long = 0,var isCompleted: Boolean = false)// 多线程下载配置data class MultiThreadConfig(val threadCount: Int = 3,val chunkSize: Long = 1024 * 1024, // 1MB per chunkval bufferSize: Int = 8192,val enableSpeedLimit: Boolean = false,val maxSpeed: Long = 1024 * 1024 // 1MB/s)// 开始多线程下载suspend fun startMultiThreadDownload(url: String,fileName: String,config: MultiThreadConfig = MultiThreadConfig(),onProgress: (DownloadState) -> Unit,onComplete: (File) -> Unit,onError: (String) -> Unit) {val taskId = generateTaskId(url, fileName)try {// 1. 检查服务器是否支持Range请求val rangeSupport = checkRangeSupport(url)if (!rangeSupport) {// 不支持Range,使用单线程下载startSingleThreadDownload(url, fileName, onProgress, onComplete, onError)return}// 2. 获取文件大小val totalSize = getFileSize(url)if (totalSize <= 0) {onError("Cannot get file size")return}// 3. 创建下载任务val task = DownloadTask(id = taskId,url = url,fileName = fileName,totalBytes = totalSize)downloadTasks[taskId] = task// 4. 分片下载val chunks = createDownloadChunks(totalSize, config.chunkSize)val tempFiles = chunks.map { chunk ->File(fileManager.getDownloadDirectory(), "${fileName}_chunk_${chunk.index}")}// 5. 启动多线程下载val jobs = chunks.mapIndexed { index, chunk ->CoroutineScope(Dispatchers.IO).launch {downloadChunk(url = url,chunk = chunk,tempFile = tempFiles[index],config = config,onChunkProgress = { downloaded ->updateTaskProgress(taskId, downloaded, totalSize, onProgress)})}}// 6. 等待所有分片下载完成jobs.joinAll()// 7. 合并文件val finalFile = mergeChunkFiles(tempFiles, fileName)// 8. 清理临时文件tempFiles.forEach { it.delete() }// 9. 完成回调onComplete(finalFile)updateTaskCompleted(taskId, finalFile, onProgress)} catch (e: Exception) {onError(e.message ?: "Download failed")updateTaskError(taskId, e.message ?: "Download failed", onProgress)}}// 创建下载分片private fun createDownloadChunks(totalSize: Long, chunkSize: Long): List<DownloadChunk> {val chunks = mutableListOf<DownloadChunk>()var startByte = 0Lvar index = 0while (startByte < totalSize) {val endByte = minOf(startByte + chunkSize - 1, totalSize - 1)chunks.add(DownloadChunk(startByte, endByte, index))startByte = endByte + 1index++}return chunks}// 下载单个分片private suspend fun downloadChunk(url: String,chunk: DownloadChunk,tempFile: File,config: MultiThreadConfig,onChunkProgress: (Long) -> Unit) {val client = networkManager.getOkHttpClient(NetworkType.DOWNLOAD_SERVER)val request = Request.Builder().url(url).addHeader("Range", "bytes=${chunk.startByte}-${chunk.endByte}").build()val response = client.newCall(request).execute()if (!response.isSuccessful) {throw Exception("Download chunk failed: ${response.code}")}val inputStream = response.body?.byteStream()val outputStream = FileOutputStream(tempFile)val buffer = ByteArray(config.bufferSize)var bytesRead: Intvar totalDownloaded = 0Lwhile (inputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {outputStream.write(buffer, 0, bytesRead)totalDownloaded += bytesReadchunk.downloadedBytes = totalDownloadedonChunkProgress(totalDownloaded)// 速度限制if (config.enableSpeedLimit) {delay(calculateDelay(bytesRead, config.maxSpeed))}}outputStream.close()inputStream?.close()chunk.isCompleted = true}// 合并分片文件private suspend fun mergeChunkFiles(chunkFiles: List<File>, fileName: String): File {val finalFile = File(fileManager.getDownloadDirectory(), fileName)val outputStream = FileOutputStream(finalFile)chunkFiles.forEach { chunkFile ->val inputStream = FileInputStream(chunkFile)inputStream.copyTo(outputStream)inputStream.close()}outputStream.close()return finalFile}// 暂停下载fun pauseDownload(taskId: String) {downloadJobs[taskId]?.cancel()val task = downloadTasks[taskId]task?.let {it.state = DownloadState.Paused(it.downloadedBytes, it.totalBytes)}}// 恢复下载fun resumeDownload(taskId: String) {val task = downloadTasks[taskId]task?.let {// 重新开始下载,支持断点续传startMultiThreadDownload(url = it.url,fileName = it.fileName,onProgress = { state -> /* 处理进度 */ },onComplete = { file -> /* 处理完成 */ },onError = { error -> /* 处理错误 */ })}}// 取消下载fun cancelDownload(taskId: String) {downloadJobs[taskId]?.cancel()downloadJobs.remove(taskId)downloadTasks.remove(taskId)}// 更新任务进度private fun updateTaskProgress(taskId: String,downloaded: Long,total: Long,onProgress: (DownloadState) -> Unit) {val task = downloadTasks[taskId] ?: returntask.downloadedBytes = downloadedval progress = ((downloaded * 100) / total).toInt()val speed = calculateSpeed(downloaded, task.startTime)val remainingTime = calculateRemainingTime(downloaded, total, speed)val state = DownloadState.Downloading(progress, downloaded, total, speed, remainingTime)task.state = stateonProgress(state)}// 计算下载速度private fun calculateSpeed(downloaded: Long, startTime: Long): Long {val elapsed = System.currentTimeMillis() - startTimereturn if (elapsed > 0) (downloaded * 1000) / elapsed else 0}// 计算剩余时间private fun calculateRemainingTime(downloaded: Long, total: Long, speed: Long): Long {return if (speed > 0) (total - downloaded) / speed else 0}// 计算延迟时间(用于速度限制)private fun calculateDelay(bytesRead: Int, maxSpeed: Long): Long {return if (maxSpeed > 0) (bytesRead * 1000) / maxSpeed else 0}private fun generateTaskId(url: String, fileName: String): String {return "${url.hashCode()}_${fileName.hashCode()}"}}
6.3 下载状态管理
sealed class DownloadState {object Idle : DownloadState()data class Downloading(val progress: Int,val downloadedBytes: Long,val totalBytes: Long,val speed: Long, // bytes per secondval remainingTime: Long // seconds) : DownloadState()data class Paused(val downloadedBytes: Long, val totalBytes: Long) : DownloadState()data class Completed(val file: File) : DownloadState()data class Error(val message: String) : DownloadState()}
7. 进度框交互
7.1 进度框状态管理
// 进度框状态sealed class ProgressDialogState {object Hidden : ProgressDialogState()data class Loading(val title: String = "加载中...",val message: String = "请稍候",val progress: Int = 0,val maxProgress: Int = 100,val isIndeterminate: Boolean = true,val showCancelButton: Boolean = false) : ProgressDialogState()data class Downloading(val title: String = "下载中...",val fileName: String,val progress: Int,val downloadedBytes: Long,val totalBytes: Long,val speed: String,val remainingTime: String,val showCancelButton: Boolean = true) : ProgressDialogState()data class Uploading(val title: String = "上传中...",val fileName: String,val progress: Int,val uploadedBytes: Long,val totalBytes: Long,val speed: String,val remainingTime: String,val showCancelButton: Boolean = true) : ProgressDialogState()data class Error(val title: String = "错误",val message: String,val showRetryButton: Boolean = true) : ProgressDialogState()data class Success(val title: String = "完成",val message: String,val showOpenButton: Boolean = false,val filePath: String? = null) : ProgressDialogState()}// 进度管理器@Singletonclass ProgressManager @Inject constructor() {private val _progressState = MutableLiveData<ProgressDialogState>()val progressState: LiveData<ProgressDialogState> = _progressStateprivate val _downloadProgress = MutableLiveData<DownloadProgress>()val downloadProgress: LiveData<DownloadProgress> = _downloadProgressprivate val _uploadProgress = MutableLiveData<UploadProgress>()val uploadProgress: LiveData<UploadProgress> = _uploadProgress// 下载进度数据类data class DownloadProgress(val fileName: String,val progress: Int,val downloadedBytes: Long,val totalBytes: Long,val speed: String,val remainingTime: String,val isPaused: Boolean = false)// 上传进度数据类data class UploadProgress(val fileName: String,val progress: Int,val uploadedBytes: Long,val totalBytes: Long,val speed: String,val remainingTime: String,val isPaused: Boolean = false)// 显示加载进度fun showLoading(title: String = "加载中...",message: String = "请稍候",showCancelButton: Boolean = false) {_progressState.value = ProgressDialogState.Loading(title = title,message = message,showCancelButton = showCancelButton)}// 显示下载进度fun showDownloading(fileName: String,progress: Int,downloadedBytes: Long,totalBytes: Long,speed: String,remainingTime: String) {_progressState.value = ProgressDialogState.Downloading(fileName = fileName,progress = progress,downloadedBytes = downloadedBytes,totalBytes = totalBytes,speed = speed,remainingTime = remainingTime)_downloadProgress.value = DownloadProgress(fileName = fileName,progress = progress,downloadedBytes = downloadedBytes,totalBytes = totalBytes,speed = speed,remainingTime = remainingTime)}// 显示上传进度fun showUploading(fileName: String,progress: Int,uploadedBytes: Long,totalBytes: Long,speed: String,remainingTime: String) {_progressState.value = ProgressDialogState.Uploading(fileName = fileName,progress = progress,uploadedBytes = uploadedBytes,totalBytes = totalBytes,speed = speed,remainingTime = remainingTime)_uploadProgress.value = UploadProgress(fileName = fileName,progress = progress,uploadedBytes = uploadedBytes,totalBytes = totalBytes,speed = speed,remainingTime = remainingTime)}// 显示错误fun showError(title: String = "错误",message: String,showRetryButton: Boolean = true) {_progressState.value = ProgressDialogState.Error(title = title,message = message,showRetryButton = showRetryButton)}// 显示成功fun showSuccess(title: String = "完成",message: String,showOpenButton: Boolean = false,filePath: String? = null) {_progressState.value = ProgressDialogState.Success(title = title,message = message,showOpenButton = showOpenButton,filePath = filePath)}// 隐藏进度框fun hideProgress() {_progressState.value = ProgressDialogState.Hidden}// 格式化文件大小fun formatFileSize(bytes: Long): String {return when {bytes < 1024 -> "$bytes B"bytes < 1024 * 1024 -> "${bytes / 1024} KB"bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"else -> "${bytes / (1024 * 1024 * 1024)} GB"}}// 格式化速度fun formatSpeed(bytesPerSecond: Long): String {return when {bytesPerSecond < 1024 -> "$bytesPerSecond B/s"bytesPerSecond < 1024 * 1024 -> "${bytesPerSecond / 1024} KB/s"bytesPerSecond < 1024 * 1024 * 1024 -> "${bytesPerSecond / (1024 * 1024)} MB/s"else -> "${bytesPerSecond / (1024 * 1024 * 1024)} GB/s"}}// 格式化剩余时间fun formatRemainingTime(seconds: Long): String {return when {seconds < 60 -> "${seconds}秒"seconds < 3600 -> "${seconds / 60}分钟"else -> "${seconds / 3600}小时${(seconds % 3600) / 60}分钟"}}}
7.2 自定义进度对话框
class CustomProgressDialog @JvmOverloads constructor(context: Context,theme: Int = R.style.CustomProgressDialog) : Dialog(context, theme) {private lateinit var binding: DialogProgressBindingprivate var onCancelClick: (() -> Unit)? = nullprivate var onRetryClick: (() -> Unit)? = nullprivate var onOpenClick: (() -> Unit)? = nullinit {initDialog()}private fun initDialog() {binding = DialogProgressBinding.inflate(layoutInflater)setContentView(binding.root)// 设置对话框属性setCancelable(false)setCanceledOnTouchOutside(false)// 设置窗口属性window?.apply {setLayout(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT)setGravity(Gravity.CENTER)setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))}setupListeners()}private fun setupListeners() {binding.btnCancel.setOnClickListener {onCancelClick?.invoke()}binding.btnRetry.setOnClickListener {onRetryClick?.invoke()}binding.btnOpen.setOnClickListener {onOpenClick?.invoke()}}fun updateState(state: ProgressDialogState) {when (state) {is ProgressDialogState.Hidden -> {dismiss()}is ProgressDialogState.Loading -> {showLoadingState(state)}is ProgressDialogState.Downloading -> {showDownloadingState(state)}is ProgressDialogState.Uploading -> {showUploadingState(state)}is ProgressDialogState.Error -> {showErrorState(state)}is ProgressDialogState.Success -> {showSuccessState(state)}}}private fun showLoadingState(state: ProgressDialogState.Loading) {binding.apply {tvTitle.text = state.titletvMessage.text = state.messageif (state.isIndeterminate) {progressBar.isIndeterminate = trueprogressBar.progress = 0} else {progressBar.isIndeterminate = falseprogressBar.max = state.maxProgressprogressBar.progress = state.progress}tvProgress.text = "${state.progress}%"// 显示/隐藏取消按钮btnCancel.visibility = if (state.showCancelButton) View.VISIBLE else View.GONE// 隐藏其他按钮btnRetry.visibility = View.GONEbtnOpen.visibility = View.GONE}if (!isShowing) show()}private fun showDownloadingState(state: ProgressDialogState.Downloading) {binding.apply {tvTitle.text = state.titletvMessage.text = state.fileNameprogressBar.isIndeterminate = falseprogressBar.max = 100progressBar.progress = state.progresstvProgress.text = "${state.progress}%"tvSpeed.text = state.speedtvRemainingTime.text = state.remainingTime// 显示取消按钮btnCancel.visibility = if (state.showCancelButton) View.VISIBLE else View.GONE// 隐藏其他按钮btnRetry.visibility = View.GONEbtnOpen.visibility = View.GONE}if (!isShowing) show()}private fun showUploadingState(state: ProgressDialogState.Uploading) {binding.apply {tvTitle.text = state.titletvMessage.text = state.fileNameprogressBar.isIndeterminate = falseprogressBar.max = 100progressBar.progress = state.progresstvProgress.text = "${state.progress}%"tvSpeed.text = state.speedtvRemainingTime.text = state.remainingTime// 显示取消按钮btnCancel.visibility = if (state.showCancelButton) View.VISIBLE else View.GONE// 隐藏其他按钮btnRetry.visibility = View.GONEbtnOpen.visibility = View.GONE}if (!isShowing) show()}private fun showErrorState(state: ProgressDialogState.Error) {binding.apply {tvTitle.text = state.titletvMessage.text = state.message// 隐藏进度条progressBar.visibility = View.GONEtvProgress.visibility = View.GONEtvSpeed.visibility = View.GONEtvRemainingTime.visibility = View.GONE// 显示重试按钮btnRetry.visibility = if (state.showRetryButton) View.VISIBLE else View.GONE// 隐藏其他按钮btnCancel.visibility = View.GONEbtnOpen.visibility = View.GONE}if (!isShowing) show()}private fun showSuccessState(state: ProgressDialogState.Success) {binding.apply {tvTitle.text = state.titletvMessage.text = state.message// 隐藏进度条progressBar.visibility = View.GONEtvProgress.visibility = View.GONEtvSpeed.visibility = View.GONEtvRemainingTime.visibility = View.GONE// 显示打开按钮btnOpen.visibility = if (state.showOpenButton) View.VISIBLE else View.GONE// 隐藏其他按钮btnCancel.visibility = View.GONEbtnRetry.visibility = View.GONE}if (!isShowing) show()}fun setOnCancelClickListener(listener: () -> Unit) {onCancelClick = listener}fun setOnRetryClickListener(listener: () -> Unit) {onRetryClick = listener}fun setOnOpenClickListener(listener: () -> Unit) {onOpenClick = listener}
}
7.3 进度框布局文件
<!-- dialog_progress.xml --><?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="24dp"android:background="@drawable/bg_progress_dialog"android:orientation="vertical"android:padding="24dp"><!-- 标题 --><TextViewandroid:id="@+id/tvTitle"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="加载中..."android:textColor="@color/text_primary"android:textSize="18sp"android:textStyle="bold"android:layout_marginBottom="8dp" /><!-- 消息 --><TextViewandroid:id="@+id/tvMessage"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="请稍候"android:textColor="@color/text_secondary"android:textSize="14sp"android:layout_marginBottom="16dp" /><!-- 进度条 --><ProgressBarandroid:id="@+id/progressBar"style="@style/Widget.AppCompat.ProgressBar.Horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginBottom="8dp" /><!-- 进度文本 --><TextViewandroid:id="@+id/tvProgress"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="0%"android:textColor="@color/text_secondary"android:textSize="12sp"android:gravity="center"android:layout_marginBottom="8dp" /><!-- 速度信息 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:layout_marginBottom="16dp"><TextViewandroid:id="@+id/tvSpeed"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="0 KB/s"android:textColor="@color/text_secondary"android:textSize="12sp" /><TextViewandroid:id="@+id/tvRemainingTime"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="剩余时间: --"android:textColor="@color/text_secondary"android:textSize="12sp" /></LinearLayout><!-- 按钮容器 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="end"><Buttonandroid:id="@+id/btnCancel"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="取消"android:textColor="@color/text_secondary"android:background="?android:attr/selectableItemBackground"android:visibility="gone"android:layout_marginEnd="8dp" /><Buttonandroid:id="@+id/btnRetry"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="重试"android:textColor="@color/colorPrimary"android:background="?android:attr/selectableItemBackground"android:visibility="gone"android:layout_marginEnd="8dp" /><Buttonandroid:id="@+id/btnOpen"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="打开"android:textColor="@color/colorPrimary"android:background="?android:attr/selectableItemBackground"android:visibility="gone" /></LinearLayout></LinearLayout>
7.4 Activity/Fragment使用示例
@AndroidEntryPointclass DownloadActivity : AppCompatActivity() {@Injectlateinit var enhancedDownloadManager: EnhancedDownloadManager@Injectlateinit var progressManager: ProgressManagerprivate lateinit var progressDialog: CustomProgressDialogoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_download)progressDialog = CustomProgressDialog(this)progressDialog.setOnCancelClickListener {// 取消下载enhancedDownloadManager.cancelDownload(currentTaskId)}progressDialog.setOnRetryClickListener {// 重试下载enhancedDownloadManager.resumeDownload(currentTaskId)}progressDialog.setOnOpenClickListener {// 打开文件openDownloadedFile()}// 观察进度状态progressManager.progressState.observe(this) { state ->progressDialog.updateState(state)}// 启动下载findViewById<Button>(R.id.btnDownload).setOnClickListener {val url = "https://example.com/large-file.zip"val fileName = "large-file.zip"enhancedDownloadManager.startDownload(url = url,fileName = fileName,onComplete = { file -> /* 处理完成 */ },onError = { error -> /* 处理错误 */ })}}private fun openDownloadedFile() {// 实现文件打开逻辑}}
8. 错误处理机制
8.1 统一错误处理
sealed class NetworkResult<T> {data class Success<T>(val data: T) : NetworkResult<T>()data class Error<T>(val message: String, val code: Int? = null) : NetworkResult<T>()class Loading<T> : NetworkResult<T>()}class NetworkBoundResource<T>(private val query: () -> LiveData<T>,private val fetch: suspend () -> T,private val saveFetchResult: suspend (T) -> Unit,private val shouldFetch: (T) -> Boolean = { true }) {fun asLiveData(): LiveData<NetworkResult<T>> = liveData {emit(NetworkResult.Loading())val dbValue = query().valueif (shouldFetch(dbValue)) {try {val fetchedValue = fetch()saveFetchResult(fetchedValue)emit(NetworkResult.Success(fetchedValue))} catch (e: Exception) {emit(NetworkResult.Error(e.message ?: "Unknown error"))}} else {emit(NetworkResult.Success(dbValue))}}}
8.2 网络状态监听
class NetworkStateMonitor @Inject constructor(private val context: Context) {private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagerfun isNetworkAvailable(): Boolean {val network = connectivityManager.activeNetworkval capabilities = connectivityManager.getNetworkCapabilities(network)return capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true}fun getNetworkType(): String {val network = connectivityManager.activeNetworkval capabilities = connectivityManager.getNetworkCapabilities(network)return when {capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "WiFi"capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "Cellular"else -> "Unknown"}}}
9. 性能优化
9.1 连接池与缓存优化
private fun createOptimizedOkHttpClient(config: NetworkConfig): OkHttpClient {return OkHttpClient.Builder().apply {// 连接池配置connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))// 超时配置connectTimeout(config.timeout, TimeUnit.SECONDS)readTimeout(config.timeout, TimeUnit.SECONDS)writeTimeout(config.timeout, TimeUnit.SECONDS)// 缓存配置cache(Cache(File(context.cacheDir, "http_cache"), 10 * 1024 * 1024))// 压缩addInterceptor { chain ->val request = chain.request().newBuilder().header("Accept-Encoding", "gzip, deflate").build()chain.proceed(request)}}.build()}
9.2 内存优化
class MemoryOptimizedDownloader {private val bufferSize = 8192private val maxMemoryUsage = 50 * 1024 * 1024 // 50MBsuspend fun downloadWithMemoryOptimization(url: String,fileName: String,onProgress: (Int) -> Unit) {val file = File(fileName)val totalSize = getFileSize(url)var downloadedBytes = 0Lval buffer = ByteArray(bufferSize)val inputStream = getInputStream(url)val outputStream = FileOutputStream(file)try {while (true) {val bytesRead = inputStream.read(buffer)if (bytesRead == -1) breakoutputStream.write(buffer, 0, bytesRead)downloadedBytes += bytesReadval progress = ((downloadedBytes * 100) / totalSize).toInt()onProgress(progress)// 内存使用检查if (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() > maxMemoryUsage) {System.gc()}}} finally {inputStream.close()outputStream.close()}}}
9.3 并发控制
class ConcurrentDownloadManager {private val downloadSemaphore = Semaphore(3) // 最多3个并发下载private val downloadJobs = mutableMapOf<String, Job>()suspend fun startDownload(url: String,fileName: String,onProgress: (Int) -> Unit,onComplete: (File) -> Unit,onError: (String) -> Unit) {downloadSemaphore.acquire()try {val job = CoroutineScope(Dispatchers.IO).launch {try {downloadFile(url, fileName, onProgress, onComplete)} catch (e: Exception) {onError(e.message ?: "Download failed")} finally {downloadSemaphore.release()}}downloadJobs[fileName] = job} catch (e: Exception) {downloadSemaphore.release()onError(e.message ?: "Failed to start download")}}fun cancelDownload(fileName: String) {downloadJobs[fileName]?.cancel()downloadJobs.remove(fileName)}}
10. 最佳实践
10.1 代码组织
- 分层清晰:UI、ViewModel、Repository、Network、Data各层职责明确
- 依赖注入:使用Hilt统一管理依赖,便于测试和替换
- 单一职责:每个类只负责一个功能,便于维护和扩展
- 接口隔离:使用多个专门的接口,避免大而全的接口
10.2 错误处理
- 统一错误格式:所有错误都有明确的错误码和错误信息
- 分级处理:网络错误、业务错误、系统错误分别处理
- 用户友好:错误信息对用户友好,便于理解和操作
- 重试机制:关键操作支持自动重试
10.3 性能优化
- 连接复用:使用OkHttp连接池复用连接
- 缓存策略:合理使用HTTP缓存和本地缓存
- 内存管理:大文件下载时分片处理,避免OOM
- 并发控制:限制并发数量,避免资源争抢
10.4 用户体验
- 进度反馈:所有耗时操作都有进度反馈
- 操作可控:支持取消、暂停、恢复等操作
- 状态清晰:用户能清楚知道当前操作的状态
- 错误友好:错误信息对用户友好,提供解决建议
10.5 扩展性
- 多BaseUrl支持:支持动态切换不同的服务器
- 插件化设计:拦截器、下载器等都支持插件化扩展
- 配置化:网络配置、下载配置等都支持动态配置
- 版本兼容:支持不同版本的API和协议
11. 扩展功能
11.1 上传功能
class UploadManager @Inject constructor(private val networkManager: NetworkManager,private val fileManager: FileManager) {suspend fun uploadFile(file: File,uploadUrl: String,onProgress: (Int) -> Unit,onComplete: (UploadResponse) -> Unit,onError: (String) -> Unit) {try {val requestBody = RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file)val multipartBody = MultipartBody.Part.createFormData("file", file.name, requestBody)val uploadService = networkManager.getRetrofit(NetworkType.UPLOAD_SERVER).create(UploadService::class.java)val response = uploadService.uploadFile(multipartBody, RequestBody.create("text/plain".toMediaTypeOrNull(), ""))if (response.isSuccessful) {onComplete(response.body()!!)} else {onError("Upload failed: ${response.code()}")}} catch (e: Exception) {onError(e.message ?: "Upload failed")}}}
11.2 任务队列
class DownloadTaskQueue @Inject constructor() {private val taskQueue = mutableListOf<DownloadTask>()private val isProcessing = AtomicBoolean(false)fun addTask(task: DownloadTask) {taskQueue.add(task)if (!isProcessing.get()) {processNextTask()}}private fun processNextTask() {if (taskQueue.isEmpty()) {isProcessing.set(false)return}isProcessing.set(true)val task = taskQueue.removeAt(0)// 执行下载任务// ...// 处理下一个任务processNextTask()}}
11.3 数据库持久化
@Entity(tableName = "download_tasks")data class DownloadTaskEntity(@PrimaryKey val id: String,val url: String,val fileName: String,val filePath: String,val downloadedBytes: Long,val totalBytes: Long,val state: String, // 序列化的状态val createdAt: Long = System.currentTimeMillis())@Daointerface DownloadTaskDao {@Query("SELECT * FROM download_tasks")suspend fun getAllTasks(): List<DownloadTaskEntity>@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insertTask(task: DownloadTaskEntity)@Updatesuspend fun updateTask(task: DownloadTaskEntity)@Deletesuspend fun deleteTask(task: DownloadTaskEntity)}
12. 总结
12.1 技术亮点
- 现代Android开发最佳实践:协程+LiveData+Hilt+分层架构
- 高可扩展性:多BaseUrl、多服务类型、动态配置
- 极致用户体验:断点续传、多线程下载、进度框交互
- 健壮性:完善的错误处理、重试机制、状态管理
- 易维护:分层清晰、依赖注入、单一职责
12.2 应用场景
- 普通API请求:RESTful接口调用
- 文件上传/下载:大文件传输,支持断点续传
- 多服务器架构:API、CDN、上传、下载服务分离
- 离线缓存:本地数据库缓存,支持离线访问
- 进度可视化:实时进度反馈,用户操作可控
12.3 团队协作
- 统一接口:所有网络请求都通过统一的接口
- 统一状态管理:进度、错误、取消等状态统一管理
- 统一错误处理:所有错误都有统一的处理方式
- 统一配置:网络配置、下载配置等统一管理
12.4 未来扩展
- WebSocket支持:实时通信功能
- GraphQL支持:更灵活的API查询
- 离线同步:支持离线操作,网络恢复后同步
- 智能缓存:根据网络状况智能调整缓存策略
- 性能监控:网络性能监控和分析
13. 常见问题与答疑
13.1 断点续传相关
Q: 服务器不支持Range请求怎么办?
A: 降级为普通下载,不支持断点续传功能。
Q: 断点信息如何持久化?
A: 使用数据库或本地文件记录下载进度。
Q: 如何防止文件损坏?
A: 下载完成前使用.tmp后缀,合并后重命名。
13.2 多线程下载相关
Q: 进度如何统计?
A: 每个chunk单独统计,合并后汇总。
Q: 失败重试如何处理?
A: 每个chunk可单独重试,不影响其他chunk。
Q: 合并文件如何保证原子性?
A: 合并后校验MD5,确保文件完整性。
13.3 性能优化相关
Q: 内存使用过高怎么办?
A: 使用分片下载,流式写入,定期GC。
Q: 网络请求频繁怎么办?
A: 使用连接池复用连接,合理使用缓存。
Q: 并发下载过多怎么办?
A: 使用信号量控制并发数量。
13.4 用户体验相关
Q: 进度不流畅怎么办?
A: 使用主线程post更新,避免频繁UI更新。
Q: 进度丢失怎么办?
A: 断点续传时恢复进度,持久化进度信息。
Q: 多任务进度如何管理?
A: 使用Map<TaskId, State>管理多个任务状态。
14. 结语
通过本套网络框架封装,开发者可以专注于业务逻辑,无需重复造轮子,极大提升开发效率和App专业度。
核心价值:
- 提升开发效率
- 改善用户体验
- 增强代码可维护性
- 支持业务快速扩展
技术特色:
- 现代化架构设计
- 完善的错误处理
- 友好的用户交互
- 高性能的网络请求