一、背景
Android 中的大文件下载需要使用分段下载,下载通常是在线程中进行的,假如有5段,那同时5个线程去执行下载,请求http返回文件流后,需要将多个文件流同时写进同一个文件,这里用到
RandomAccessFile。
分段上传的话,只需要根据每段文件阀值,例如,50M为一段,将文件按照设置的阀值,分段上传即可
二、相关代码
2.1 分段上传关键代码
忽略网络请求和状态码,每个人接口定义的的请求参数和返回code不一样
private val DEFAULT_BLOCK_SIZE: Long = 50 * 1024 * 1024 //50MB
val blockSize=DEFAULT_BLOCK_SIZE
val randomAccessFile = RandomAccessFile(filePath, "r")
val fileLen = randomAccessFile.length()
//超过设定的单个文件大小,需要分块上传
val blockFileNum = Math.ceil((fileLen / blockSize.toDouble())).toInt()
XLogUtil.d("${TAG}blockFileNum:$blockFileNum,,,,fileLen:$fileLen,,,blockSize:$blockSize,,,requestId:$requestId")
var offSet = 0L
var successNum = 0
var isSendResult = true
for (i in 0 until blockFileNum) {
val startOffset = i * blockSize
val blockFileLen = Math.min(blockSize, fileLen - startOffset)
val fileData = getFileData(filePath, offSet, blockFileLen.toInt())
// 创建文件名请求体
val requestBody = RequestBody.create(null, fileData)
val call = RetrofitClient.getUploadFileService(
token,
requestId,
offSet.toString(),
uploadType
).uploadFile(file.name, requestBody)
XLogUtil.d("${TAG}upload 第${i + 1}块 block file,offSet:$offSet,,,blockFileLen:$blockFileLen,,,blockFileNum:$blockFileNum,,,fileLen:$fileLen,,,filePath:$filePath,,,fileData size:${fileData?.size},,,requestId:$requestId")
offSet += blockFileLen
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
val code = response.code()
XLogUtil.d("${TAG}upload 第${i + 1}块 block file result code:$code,,,requestId:$requestId")
if (code == 201) {
//处理成功响应
successNum++
if (successNum == blockFileNum) {
XLogUtil.d("${TAG}upload all block file success,blockFileNum:$blockFileNum,,,requestId:$requestId")
listener?.apply {
onSuccess(Constant.SUCCESS, requestId)
}
//上传完
}
} else {
//处理失败响应
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// 处理请求失败
XLogUtil.d("${TAG}upload 第${i + 1}块 block file onFailure message:${t.printStackTrace()}")
}
})
}
/**
* 根据偏移量获取分块文件数据
*/
fun getFileData(filePath: String, offset: Long, length: Int): ByteArray? {
// 使用RandomAccessFile随机访问文件
var randomAccessFile: RandomAccessFile? = null
try {
randomAccessFile = RandomAccessFile(filePath, "r")
val fileChannel = randomAccessFile.channel
// 将文件的部分区域映射为内存区域
val mappedByteBuffer =
fileChannel.map(FileChannel.MapMode.READ_ONLY, offset, length.toLong())
val data = ByteArray(length)
// 从映射区域中读取数据
mappedByteBuffer[data]
return data
} catch (e: Exception) {
e.printStackTrace()
return null
} finally {
if (randomAccessFile != null) {
try {
randomAccessFile.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
2.2、分段下载关键代码
val folder = File(DOWNLOAD_FOLDER_PATH)
if (!folder.exists()) {
folder.mkdirs()
}
val fileName = getFileNameWithPath(filePath)
val downFile = File(DOWNLOAD_FOLDER_PATH + File.separator + fileName)
if (!downFile.exists()) {
downFile.createNewFile()
}
// 使用输入流保存响应体到文件,这里通常是通过http请求,返回的文件流,替换即可
val inputStream = body.byteStream()
val rw = RandomAccessFile(downFile, "rw")
rw.seek(startPosition)//文件写入的初始位置
var hasReads = 0
var readLenght: Long = 0
val bytes = ByteArray(4096)
while ((inputStream.read(bytes).also { hasReads = it }) > 0) {
rw.write(bytes, 0, hasReads)
readLenght += hasReads
// val l = (readLenght * 100 / contentLength) as Int 单块文件写入进度
}
// 关闭文件输出流和输入流
inputStream.close()
rw.close()
/**
* 根据文件路径获取文件名
*/
fun getFileNameWithPath(path: String): String {
if (TextUtils.isEmpty(path)) {
return ""
}
val start = path.lastIndexOf("/")
return if (start != -1) {
path.substring(start + 1)
} else {
"DEFAULT_NAME"
}
}