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

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

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

技术架构图

客户端 ProgressRequestBody CountingSink OkHttp原始Sink 网络传输 ProgressListener 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. 高级应用如断点续传和速度计算

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

相关推荐
重整旗鼓~3 分钟前
7.索引库操作
java·开发语言
Jay Kay22 分钟前
深入解析协程:高并发编程的轻量级解决方案
开发语言·c++·算法
lifallen1 小时前
Java BitSet类解析:高效位向量实现
java·开发语言·后端·算法
whysqwhw1 小时前
Egloo 中Kotlin 多平台中的 expect/actual
android
用户2018792831671 小时前
《Android 城堡防御战:ProGuard 骑士的代码混淆魔法》
android
学不好python的小猫1 小时前
7-4 身份证号处理
开发语言·python·算法
teeeeeeemo1 小时前
JS数据类型检测方法总结
开发语言·前端·javascript·笔记
Junerver1 小时前
如何在Jetpack Compose中轻松的进行表单验证
前端·kotlin
用户2018792831672 小时前
🔐 加密特工行动:Android 中的 AES 与 RSA 秘密行动指南
android
一只帆記2 小时前
Java 实现后端调用 Chromium 浏览器无头模式截图的方案
java·开发语言