1、重写文件下载,优化下载算法
This commit is contained in:
shenzuqiang 2026-03-16 11:56:42 +08:00
parent 589f5f08e5
commit cbcfc0c315
5 changed files with 132 additions and 426 deletions

View File

@ -1,385 +0,0 @@
package com.img.rabbit.utils
import android.os.Handler
import android.os.Looper
import android.util.Log
import okhttp3.*
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.io.*
import java.net.URL
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit
class DownLoadUtils private constructor() {
companion object {
private const val TAG = "DownLoadUtils"
private val downLoadHttpUtils: DownLoadUtils by lazy {
DownLoadUtils()
}
@JvmStatic
@Synchronized
fun getInstance(): DownLoadUtils {
return downLoadHttpUtils
}
}
private var downloadSizeInfo = mutableMapOf<String, Long>()
private var cancelledList = mutableListOf<String>()
private var buffSize = 4096//建议设置为2048
fun setBuffSize(size: Int): DownLoadUtils {
this.buffSize = size
return this
}
private var interceptor: Interceptor? = null
fun setInterceptor(interceptor: Interceptor?): DownLoadUtils {
this.interceptor = interceptor
return this
}
private var readTimeOut = 30L
fun setReadTImeOut(read: Long): DownLoadUtils {
this.readTimeOut = read
return this
}
private var writeTimeout = 30L
fun setWriteTimeOut(write: Long): DownLoadUtils {
this.writeTimeout = write
return this
}
private var connectTimeout = 30L
fun setConnectTimeOut(connect: Long): DownLoadUtils {
this.connectTimeout = connect
return this
}
var filePath = ""
fun setFilePath(path: String): DownLoadUtils {
this.filePath = path
return this
}
private var fileName = ""
fun setFileName(name: String): DownLoadUtils {
this.fileName = name
return this
}
private var deleteWhenException = true
fun setDeleteWhenException(dele: Boolean): DownLoadUtils {
this.deleteWhenException = dele
return this
}
private val requestBuilder: Request.Builder = Request.Builder()
private var urlBuilder: HttpUrl.Builder? = null
private val okHttpClient = lazy {
OkHttpClient.Builder()
.readTimeout(readTimeOut, TimeUnit.SECONDS)
.writeTimeout(writeTimeout, TimeUnit.SECONDS)
.connectTimeout(connectTimeout, TimeUnit.SECONDS)
.addInterceptor(interceptor ?: LoggingInterceptor())
.build()
}
private var actionGetTotal: (total: Long) -> Unit? = { _ -> }
private var actionProgress: (position: Long) -> Unit? = { _ -> }
private var actionSuccess: (file: File) -> Unit? = { _ -> }
private var actionCancel: () -> Unit? = { }
private var actionFail: (msg: String) -> Unit? = { _ -> }
fun setActionCallBack(
actionGetTotal: (total: Long) -> Unit,
actionProgress: (position: Long) -> Unit,
actionSuccess: (file: File) -> Unit,
actionFail: (msg: String) -> Unit,
): DownLoadUtils {
this.actionGetTotal = actionGetTotal
this.actionProgress = actionProgress
this.actionSuccess = actionSuccess
this.actionFail = actionFail
return this
}
private var downCallBack: DownCallBack? = null
fun setDownCallBack(callBack: DownCallBack): DownLoadUtils {
this.downCallBack = callBack
return this
}
fun initUrl(url: String, params: Map<String, String>?): DownLoadUtils {
urlBuilder = url.toHttpUrlOrNull()?.newBuilder()
if (params.isNullOrEmpty()) {
return this
} else {
for ((k, v) in params) {
checkName(k)
urlBuilder?.setQueryParameter(k, v)
}
}
return this
}
fun addHeader(map: Map<String, String>): DownLoadUtils {
requestBuilder.headers(map.toHeaders())
return this
}
private fun checkName(name: String) {
require(name.isNotEmpty()) { "name is empty" }
}
fun down() {
if (urlBuilder == null) {
throw IllegalStateException("url not init")
} else {
doDown()
}
}
private fun doDown() {
val startTime = System.currentTimeMillis()
Log.i(TAG, "startTime=$startTime")
val url = urlBuilder?.build()
if (url == null) {
doException("url is null")
return
}
if (isDowning(filePath + fileName)) {
return
}
cancelledList.remove(fileName)
val currentLen = downloadSizeInfo[fileName] ?: 0L
if (isCanContinueDownload(url.toUrl(), currentLen)) {
requestBuilder.removeHeader("RANGE")
requestBuilder.addHeader("RANGE", "bytes=${currentLen}-")
}
val request = requestBuilder.url(url).tag(filePath + fileName).build()
var `is`: InputStream? = null
var raf: RandomAccessFile? = null
var file: File? = null
try {
val response = okHttpClient.value.newCall(request).execute()
val total = response.body?.contentLength() ?: 0
doGetTotal(currentLen + total)
val buf = ByteArray(buffSize)
var len: Int
file = if (fileName.isEmpty()) {
File(filePath)
} else {
val fileDir = File(filePath)
if (!fileDir.exists() || !fileDir.isDirectory) {
fileDir.mkdirs()
}
File(filePath, fileName)
}
`is` = response.body?.byteStream() ?: FileInputStream("")
raf = RandomAccessFile(file, "rw")
raf.seek(currentLen)
var sum: Long = currentLen
while (`is`.read(buf).also { len = it } != -1) {
if (isCancelled()) throw CancellationException()
raf.write(buf, 0, len)
sum += len.toLong()
downloadSizeInfo[fileName] = sum
Log.e(TAG, "download progress : $sum")
doProgress(sum)
}
Log.e(TAG, "download success")
if (!file.exists()) {
throw FileNotFoundException("file create err,not exists")
} else {
doSuccess(file)
}
Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime))
} catch (e: Exception) {
if (deleteWhenException && file?.exists() == true) file.delete()
Log.e(TAG, "download failed : " + e.message)
doException(e.message.toString())
} finally {
try {
`is`?.close()
} catch (e: IOException) {
e.printStackTrace()
}
try {
raf?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun isCanContinueDownload(url: URL, start: Long): Boolean { //是否支持断点下载
val requestBuilder = Request.Builder()
requestBuilder.addHeader("RANGE", "bytes=$start-")
requestBuilder.addHeader("Connection", "close")
val request: Request = requestBuilder.url(url).head().build()
val response: Response = okHttpClient.value.newCall(request).execute()
return if (response.isSuccessful) {
if (response.code == 206) { //支持
response.close()
true
} else { //不支持
response.close()
false
}
} else {
response.close()
false
}
}
private fun isDowning(tag: String): Boolean {
for (call in okHttpClient.value.dispatcher.runningCalls()) {
if (call.request().tag() == tag) {
return true
}
}
return false
}
private fun isCancelled(): Boolean {
return cancelledList.contains(fileName)
}
fun cancel() {
cancel(filePath + fileName)
}
fun cancel(tag: String) {
cancelledList.add(fileName)
if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) {
for (call in okHttpClient.value.dispatcher.runningCalls()) {
if (call.request().tag() == tag) {
call.cancel()
}
}
}
doCancel()
}
private fun doException(err: String) {
runOnUiThread {
if (downCallBack == null) {
actionFail.invoke(err)
} else {
downCallBack?.fail(err)
}
mainThread = null
}
}
private fun doSuccess(file: File?) {
runOnUiThread {
if (file == null) {
doException("file not exit")
} else {
if (downCallBack == null) {
actionSuccess.invoke(file)
} else {
downCallBack?.success(file)
}
}
mainThread = null
}
}
private fun doGetTotal(total: Long) {
runOnUiThread {
if (downCallBack == null) {
actionGetTotal.invoke(total)
} else {
downCallBack?.total(total)
}
}
mainThread = null
}
private fun doProgress(progress: Long) {
runOnUiThread {
if (downCallBack == null) {
actionProgress.invoke(progress)
} else {
downCallBack?.progress(progress)
}
}
}
private fun doCancel() {
runOnUiThread {
if (downCallBack == null) {
actionCancel.invoke()
} else {
downCallBack?.cancel()
}
}
mainThread = null
}
private var mainThread: Handler? = null
private fun runOnUiThread(action: () -> Unit) {
if (Looper.myLooper() != Looper.getMainLooper()) { // If we finish marking off of the main thread, we need to
// actually do it on the main thread to ensure correct ordering.
if (mainThread == null) {
mainThread = Handler(Looper.getMainLooper())
}
mainThread?.post {
action.invoke()
}
return
}
action.invoke()
}
class LoggingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val startTime = System.nanoTime()
Log.d(
TAG, String.format(
"Sending request %s on %s%n%s",
request.url, chain.connection(), request.headers
)
)
val response: Response = chain.proceed(request)
val endTime = System.nanoTime()
Log.d(
TAG, String.format(
"Received response for %s in %.1fms%n%s",
response.request.url, (endTime - startTime) / 1e6, response.headers
)
)
return response
}
companion object {
private const val TAG = "LoggingInterceptor"
}
}
interface DownCallBack {
fun success(file: File)
fun fail(str: String)
fun progress(position: Long)
fun total(total: Long)
fun cancel()
}
}

View File

@ -0,0 +1,92 @@
package com.img.rabbit.utils
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.buffer
import okio.sink
import java.io.File
import java.io.IOException
object FileDownloadUtil {
private val client = OkHttpClient.Builder().build()
fun download(url: String, saveFile: File, listener: DownloadListener) {
val request = Request.Builder().url(url).build()
// 拦截器处理进度
val progressClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
.build()
progressClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
listener.onError(e)
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
listener.onError(IOException("Unexpected code $response"))
return
}
try {
val totalSize = response.body.contentLength()
listener.onStart(totalSize)
response.body.source().use { source ->
saveFile.sink().buffer().use { sink ->
sink.writeAll(source)
}
}
listener.onSuccess(saveFile)
} catch (e: Exception) {
listener.onError(e)
}
}
})
}
}
// 进度回调接口
interface DownloadListener {
fun onStart(totalSize: Long) // 开始下载,返回文件总大小
fun onProgress(progress: Int, current: Long) // 进度(0-100),当前已下载字节
fun onSuccess(file: File) // 下载成功
fun onError(e: Exception) // 下载失败
}
// 自定义 ResponseBody 用于拦截进度
class ProgressResponseBody(
private val responseBody: ResponseBody,
private val listener: DownloadListener
) : ResponseBody() {
override fun contentType() = responseBody.contentType()
override fun contentLength() = responseBody.contentLength()
override fun source(): BufferedSource {
return object : ForwardingSource(responseBody.source()) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
val progress = if (contentLength() > 0) (totalBytesRead * 100 / contentLength()).toInt() else 0
// 回调进度
listener.onProgress(progress, totalBytesRead)
return bytesRead
}
}.buffer()
}
}

View File

@ -307,7 +307,7 @@ object UniAppUtils {
oldFile.delete()
}
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, 0.01f)
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, 0.001f)
scope.launch {
val isAvailable = isFileDownloadable(uniVersion.url)

View File

@ -16,25 +16,27 @@ object UpdateUtils {
@SuppressLint("SetTextI18n")
fun download(scope: CoroutineScope, url: String, filePath: String, fileName: String, onProgress:(progress:Int)-> Unit, onFinish:(isSuccess: Boolean, filePath: String?)-> Unit) {
scope.launch(Dispatchers.IO) {
var totalProgress = 0L
DownLoadUtils.getInstance()
.setReadTImeOut(10L)
.setDeleteWhenException(false)
.initUrl(url, null)
.setFilePath(filePath)
.setFileName(fileName)
.setActionCallBack(
{ totalProgress = it },
{
val percent = it.toDouble() / totalProgress.toDouble() * 100
val curProgress = percent.toInt()
onProgress(curProgress)
},
{
onFinish(true, it.absolutePath)
}, {
val destination = File(filePath, fileName)
if(destination.exists()){
destination.delete()
}
FileDownloadUtil.download(url, destination, object : DownloadListener {
override fun onStart(totalSize: Long) {
onProgress(0)
}
override fun onProgress(progress: Int, current: Long) {
onProgress(progress)
}
override fun onSuccess(file: File) {
onFinish(true, file.absolutePath)
}
override fun onError(e: Exception) {
onFinish(false, null)
}).down()
}
})
}
}

View File

@ -6,12 +6,16 @@ import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import com.img.rabbit.bean.local.ErrorBean
import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.utils.DownLoadUtils
import com.img.rabbit.utils.DownloadListener
import com.img.rabbit.utils.FileDownloadUtil
import com.img.rabbit.utils.FileUtils
import com.img.rabbit.utils.ImageUtils
import com.img.rabbit.utils.PhotoCutter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
@ -99,34 +103,27 @@ class CutoutViewModel : BaseViewModel() {
if(targetFile.exists()){
targetFile.delete()
}
DownLoadUtils.getInstance()
.initUrl(url, null)
.setFilePath(targetPath)
.setFileName(targetFileName)
.setDownCallBack(object : DownLoadUtils.DownCallBack {
override fun success(file: File) {
viewModelScope.launch(Dispatchers.IO) {
FileDownloadUtil.download(url, targetFile, object : DownloadListener {
override fun onStart(totalSize: Long) {
}
override fun onProgress(progress: Int, current: Long) {
// 如果需要进度可以在这里处理
}
override fun onSuccess(file: File) {
// 3. 下载成功后,将文件解析为 Bitmap 并回调给 UI
val bitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
onResult(true, bitmap)
}
override fun fail(str: String) {
onResult(false, null)
}
override fun progress(position: Long) {
// 如果需要进度可以在这里处理
}
override fun total(total: Long) {
// 获取总大小
}
override fun cancel() {
override fun onError(e: Exception) {
onResult(false, null)
}
})
.down()
}
}catch (e: Exception){
e.printStackTrace()
onResult(false, null)