Kotlin实现文件上传进度监听:RequestBody封装详解

引言:为什么需要文件上传进度监听?

在移动应用开发中,文件上传是常见需求。特别是当用户上传大文件(如视频、高清图片)时,缺乏进度反馈会导致糟糕的用户体验。本文将深入探讨如何通过封装OkHttp的RequestBody实现高效、灵活的文件上传进度监听。

技术架构图

graph TD A[客户端] --> B[ProgressRequestBody] B --> C[CountingSink] C --> D[OkHttp原始Sink] D --> E[网络传输] B --> F[ProgressListener] F --> G[UI更新]

关键技术组件

  1. ProgressListener:进度回调接口
  2. CountingSink:字节计数拦截器
  3. ProgressRequestBody:RequestBody封装器

完整代码实现

1. 进度监听接口

kotlin 复制代码
interface ProgressListener {
    fun onProgress(
        bytesWritten: Long, 
        contentLength: Long, 
        identifier: String? = null,
        done: Boolean = false
    )
}

2. 字节计数Sink实现

kotlin 复制代码
import okio.ForwardingSink
import okio.Sink
import java.io.IOException

class CountingSink(
    delegate: Sink,
    private val listener: ProgressListener,
    private val contentLength: Long,
    private val identifier: String? = null
) : ForwardingSink(delegate) {

    private var bytesWritten = 0L
    private var lastReportedPercent = -1

    @Throws(IOException::class)
    override fun write(source: Buffer, byteCount: Long) {
        super.write(source, byteCount)
        bytesWritten += byteCount
        
        // 优化:每1%进度回调一次,避免频繁更新UI
        val currentPercent = (100 * bytesWritten / contentLength).toInt()
        if (currentPercent != lastReportedPercent) {
            listener.onProgress(bytesWritten, contentLength, identifier)
            lastReportedPercent = currentPercent
        }
    }

    @Throws(IOException::class)
    override fun close() {
        super.close()
        // 最终完成回调
        listener.onProgress(bytesWritten, contentLength, identifier, true)
    }
}

3. 可监听进度的RequestBody封装

kotlin 复制代码
import okhttp3.MediaType
import okhttp3.RequestBody
import okio.BufferedSink
import okio.Okio

class ProgressRequestBody(
    private val delegate: RequestBody,
    private val listener: ProgressListener,
    private val identifier: String? = null
) : RequestBody() {

    override fun contentType(): MediaType? = delegate.contentType()

    override fun contentLength(): Long = delegate.contentLength()

    @Throws(IOException::class)
    override fun writeTo(sink: BufferedSink) {
        val countingSink = CountingSink(
            sink, 
            listener, 
            contentLength(),
            identifier
        )
        val bufferedSink = Okio.buffer(countingSink)
        
        delegate.writeTo(bufferedSink)
        
        // 确保所有数据写入
        bufferedSink.flush()
    }
}

实战应用:多文件上传示例

Retrofit服务接口定义

kotlin 复制代码
interface UploadService {
    @Multipart
    @POST("upload")
    suspend fun uploadFiles(
        @Part files: List<MultipartBody.Part>
    ): Response<UploadResult>
}

多文件上传管理器

kotlin 复制代码
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MultipartBody
import retrofit2.Response
import java.io.File

class UploadManager(
    private val service: UploadService,
    private val uiHandler: Handler = Handler(Looper.getMainLooper())
) {
    
    // 进度回调映射:文件路径 -> 进度百分比
    private val progressMap = mutableMapOf<String, Int>()
    
    // 进度监听器实现
    private val progressListener = object : ProgressListener {
        override fun onProgress(bytesWritten: Long, contentLength: Long, identifier: String?, done: Boolean) {
            identifier?.let { filePath ->
                val progress = (100 * bytesWritten / contentLength).toInt()
                progressMap[filePath] = progress
                
                // 更新UI(切换到主线程)
                uiHandler.post {
                    // 这里简化处理,实际应通过LiveData或回调通知
                    println("文件 $filePath 上传进度: $progress%")
                }
            }
        }
    }

    suspend fun uploadFiles(files: List<File>): UploadResult {
        return withContext(Dispatchers.IO) {
            // 准备上传部件
            val parts = files.map { file ->
                val requestBody = file.asRequestBody("application/octet-stream".toMediaType())
                val progressBody = ProgressRequestBody(
                    requestBody,
                    progressListener,
                    file.absolutePath // 使用文件路径作为标识符
                )
                MultipartBody.Part.createFormData(
                    "files", 
                    file.name, 
                    progressBody
                )
            }
            
            // 执行上传
            val response = service.uploadFiles(parts)
            
            if (response.isSuccessful) {
                response.body() ?: throw UploadException("上传成功但返回空数据")
            } else {
                throw UploadException("上传失败: ${response.code()}")
            }
        }
    }
}

class UploadException(message: String) : Exception(message)

Android中使用示例

kotlin 复制代码
// 在ViewModel中
class UploadViewModel : ViewModel() {
    private val uploadManager = UploadManager(RetrofitClient.uploadService)
    
    // 使用LiveData跟踪进度和结果
    val uploadProgress = MutableLiveData<Map<String, Int>>()
    val uploadResult = MutableLiveData<Result<UploadResult>>()
    
    fun uploadFiles(files: List<File>) {
        viewModelScope.launch {
            uploadResult.value = Result.loading()
            try {
                val result = uploadManager.uploadFiles(files)
                uploadResult.value = Result.success(result)
            } catch (e: Exception) {
                uploadResult.value = Result.error(e)
            }
        }
    }
    
    // 更新进度(实际应用中UploadManager应回调此方法)
    fun updateProgress(progressMap: Map<String, Int>) {
        uploadProgress.value = progressMap
    }
}

// 在Activity/Fragment中观察
viewModel.uploadProgress.observe(this) { progressMap ->
    progressMap.forEach { (filePath, progress) ->
        // 更新对应文件的进度条
        fileProgressBars[filePath]?.progress = progress
    }
}

viewModel.uploadResult.observe(this) { result ->
    when (result.status) {
        Status.LOADING -> showLoading()
        Status.SUCCESS -> showSuccess(result.data)
        Status.ERROR -> showError(result.message)
    }
}

性能优化策略

1. 回调频率控制

kotlin 复制代码
// 在CountingSink中添加优化逻辑
private var lastReportedTime = 0L
private val REPORT_INTERVAL = 200L // 200毫秒

override fun write(source: Buffer, byteCount: Long) {
    super.write(source, byteCount)
    bytesWritten += byteCount
    
    val currentTime = System.currentTimeMillis()
    if (currentTime - lastReportedTime > REPORT_INTERVAL || bytesWritten == contentLength) {
        listener.onProgress(bytesWritten, contentLength, identifier)
        lastReportedTime = currentTime
    }
}

2. 弱引用防止内存泄漏

kotlin 复制代码
class WeakProgressListener(
    private val delegate: ProgressListener
) : ProgressListener {
    
    private val weakRef = WeakReference(delegate)
    
    override fun onProgress(bytesWritten: Long, contentLength: Long, identifier: String?, done: Boolean) {
        weakRef.get()?.onProgress(bytesWritten, contentLength, identifier, done)
    }
}

// 使用方式
val progressBody = ProgressRequestBody(
    requestBody,
    WeakProgressListener(progressListener),
    file.absolutePath
)

3. 大文件分块上传

kotlin 复制代码
class ChunkedUploader(
    private val file: File,
    private val chunkSize: Long = 1024 * 1024 // 1MB
) {
    suspend fun uploadWithProgress(listener: ProgressListener) {
        val totalSize = file.length()
        var uploaded = 0L
        var chunkIndex = 0
        
        file.inputStream().use { input ->
            val buffer = ByteArray(chunkSize.toInt())
            var bytesRead: Int
            
            while (input.read(buffer).also { bytesRead = it } != -1) {
                // 上传当前分块
                uploadChunk(chunkIndex, buffer, bytesRead)
                
                // 更新进度
                uploaded += bytesRead
                listener.onProgress(uploaded, totalSize, file.absolutePath)
                
                chunkIndex++
            }
        }
        
        // 最终完成回调
        listener.onProgress(totalSize, totalSize, file.absolutePath, true)
    }
    
    private suspend fun uploadChunk(index: Int, data: ByteArray, size: Int) {
        // 实现分块上传逻辑
    }
}

与其他技术对比

技术方案 优点 缺点 适用场景
RequestBody封装 底层实现、高效灵活、与OkHttp无缝集成 需要自定义封装、对新手有一定难度 需要精细控制上传过程的场景
Interceptor拦截器 统一处理所有请求、位置集中 难以区分不同文件进度、实现复杂 需要全局监控的场景
系统级进度监听 简单易用、无需额外代码 功能有限、无法自定义回调频率 简单小文件上传
分块上传+自定义协议 支持断点续传、精确控制 实现复杂、需要服务端配合 超大文件上传、不稳定网络环境

关键点总结

  1. 核心原理:通过自定义Sink拦截写入操作实现字节计数
  2. 进度计算进度百分比 = (已上传字节数 / 文件总大小) * 100
  3. 线程安全:进度回调在IO线程,更新UI需切主线程
  4. 性能优化
    • 控制回调频率(时间阈值或进度阈值)
    • 使用弱引用防止内存泄漏
    • 大文件采用分块上传策略
  5. 多文件支持:为每个文件分配唯一标识符
  6. 容错处理:处理除零异常和取消上传逻辑

高级应用场景

1. 断点续传实现

kotlin 复制代码
class ResumableProgressRequestBody(
    delegate: RequestBody,
    listener: ProgressListener,
    identifier: String?,
    private val startPosition: Long // 从上次中断处继续
) : ProgressRequestBody(delegate, listener, identifier) {

    override fun writeTo(sink: BufferedSink) {
        val countingSink = CountingSink(
            sink, 
            listener, 
            contentLength(),
            identifier
        ).apply {
            bytesWritten = startPosition // 设置起始位置
        }
        
        val bufferedSink = Okio.buffer(countingSink)
        delegate.writeTo(bufferedSink)
        bufferedSink.flush()
    }
}

2. 上传速度计算

kotlin 复制代码
// 在ProgressListener中添加速度回调
interface AdvancedProgressListener : ProgressListener {
    fun onSpeedCalculated(bytesPerSecond: Double, identifier: String?)
}

// 在CountingSink中实现速度计算
private var lastBytesWritten = 0L
private var lastTimeMillis = System.currentTimeMillis()

override fun write(source: Buffer, byteCount: Long) {
    super.write(source, byteCount)
    
    val currentTime = System.currentTimeMillis()
    val elapsed = currentTime - lastTimeMillis
    
    if (elapsed > 1000) { // 每秒计算一次
        val deltaBytes = bytesWritten - lastBytesWritten
        val speed = deltaBytes / (elapsed / 1000.0)
        
        (listener as? AdvancedProgressListener)?.onSpeedCalculated(speed, identifier)
        
        lastBytesWritten = bytesWritten
        lastTimeMillis = currentTime
    }
}

最佳实践建议

  1. 进度显示策略

    • 小文件(<5MB):显示百分比
    • 中等文件(5-50MB):显示百分比+剩余时间
    • 大文件(>50MB):显示百分比+速度+剩余时间
  2. 异常处理增强

    kotlin 复制代码
    override fun writeTo(sink: BufferedSink) {
        try {
            // ... 正常写入逻辑
        } catch (e: IOException) {
            listener.onError(e, identifier)
            throw e
        } finally {
            // 清理资源
        }
    }
  3. 取消上传支持

    kotlin 复制代码
    class CancellableProgressRequestBody(
        delegate: RequestBody,
        listener: ProgressListener,
        identifier: String?,
        private val isCancelled: () -> Boolean // 取消状态检查
    ) : ProgressRequestBody(delegate, listener, identifier) {
    
        @Throws(IOException::class)
        override fun writeTo(sink: BufferedSink) {
            val countingSink = object : CountingSink(sink, listener, contentLength(), identifier) {
                override fun write(source: Buffer, byteCount: Long) {
                    if (isCancelled()) throw IOException("Upload cancelled")
                    super.write(source, byteCount)
                }
            }
            // ... 其余逻辑
        }
    }

总结

本文详细介绍了通过封装OkHttp的RequestBody实现文件上传进度监听的技术方案。核心要点包括:

  1. 通过自定义CountingSink拦截和计算已上传字节数
  2. 使用ProgressRequestBody包装原始RequestBody
  3. 实现多文件上传的进度区分
  4. 性能优化和内存管理技巧
  5. 高级应用如断点续传和速度计算

这种方案具有高度灵活性和可扩展性,能够满足从简单到复杂的各种文件上传需求。

相关推荐
积跬步DEV4 小时前
Android 获取签名 keystore 的 SHA1和MD5值
android
陈旭金-小金子6 小时前
发现 Kotlin MultiPlatform 的一点小变化
android·开发语言·kotlin
二流小码农8 小时前
鸿蒙开发:DevEcoStudio中的代码提取
android·ios·harmonyos
江湖有缘9 小时前
使用obsutil工具在OBS上完成基本的数据存取【玩转华为云】
android·java·华为云
移动开发者1号10 小时前
Android 多 BaseUrl 动态切换策略(结合 ServiceManager 实现)
android·kotlin
AJi13 小时前
Android音视频框架探索(三):系统播放器MediaPlayer的创建流程
android·ffmpeg·音视频开发
柿蒂14 小时前
WorkManager 任务链详解:优雅处理云相册上传队列
android
alexhilton14 小时前
使用用例(Use Case)以让Android代码更简洁
android·kotlin·android jetpack
峥嵘life14 小时前
Android xml的Preference设置visibility=“gone“ 无效分析解决
android·xml