1、下载器优化
This commit is contained in:
shenzuqiang 2026-03-16 16:45:22 +08:00
parent 9bdb30e28f
commit d751ec6c6d
4 changed files with 163 additions and 288 deletions

View File

@ -245,7 +245,6 @@ class MainActivity : ComponentActivity(), LoadingCallback {
//资源下载进度 //资源下载进度
if (progress != null) { if (progress != null) {
progressWGTToPageState.floatValue = progress progressWGTToPageState.floatValue = progress
Log.i("HomeScreen", "DOWNLOAD_PROGRESS:$progress")
} }
} }

View File

@ -235,7 +235,6 @@ fun TipsUniMpDialog(
//资源下载进度 //资源下载进度
if(progress != null){ if(progress != null){
downProgress.value = progress downProgress.value = progress
Log.i("HomeScreen","DOWNLOAD_PROGRESS:$progress")
} }
} }

View File

@ -4,84 +4,47 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import okhttp3.* import okhttp3.*
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.io.* import java.io.*
import java.net.URL import java.net.URL
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class DownUtils private constructor() { class DownUtils private constructor() {
companion object { companion object {
private const val TAG = "DownLoadUtils" private const val TAG = "DownUtils"
private val downLoadHttpUtils: DownUtils by lazy { private val _instance: DownUtils by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DownUtils() DownUtils()
} }
@JvmStatic @JvmStatic
@Synchronized fun getInstance(): DownUtils = _instance
fun getInstance(): DownUtils {
return downLoadHttpUtils
}
} }
private var downloadSizeInfo = mutableMapOf<String, Long>() // 1. 线程安全集合:防止并发修改异常
private var cancelledList = mutableListOf<String>() private val downloadSizeInfo = ConcurrentHashMap<String, Long>()
private val cancelledList = CopyOnWriteArrayList<String>()
private var buffSize = 4096//建议设置为2048 // 2. 常驻 Handler解决 runOnUiThread 随机崩溃
fun setBuffSize(size: Int): DownUtils { private val mainHandler = Handler(Looper.getMainLooper())
this.buffSize = size
return this
}
private var interceptor: Interceptor? = null
fun setInterceptor(interceptor: Interceptor?): DownUtils {
this.interceptor = interceptor
return this
}
private var buffSize = 8192
private var readTimeOut = 30L private var readTimeOut = 30L
fun setReadTImeOut(read: Long): DownUtils {
this.readTimeOut = read
return this
}
private var writeTimeout = 30L private var writeTimeout = 30L
fun setWriteTimeOut(write: Long): DownUtils {
this.writeTimeout = write
return this
}
private var connectTimeout = 30L private var connectTimeout = 30L
fun setConnectTimeOut(connect: Long): DownUtils { private var interceptor: Interceptor? = null
this.connectTimeout = connect
return this
}
var filePath = ""
fun setFilePath(path: String): DownUtils {
this.filePath = path
return this
}
// 任务参数
private var filePath = ""
private var fileName = "" private var fileName = ""
fun setFileName(name: String): DownUtils { private var deleteWhenException = false
this.fileName = name
return this
}
private var deleteWhenException = true
fun setDeleteWhenException(dele: Boolean): DownUtils {
this.deleteWhenException = dele
return this
}
private val requestBuilder: Request.Builder = Request.Builder()
private var urlBuilder: HttpUrl.Builder? = null private var urlBuilder: HttpUrl.Builder? = null
private val headers = mutableMapOf<String, String>()
private val okHttpClient = lazy { private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder() OkHttpClient.Builder()
.readTimeout(readTimeOut, TimeUnit.SECONDS) .readTimeout(readTimeOut, TimeUnit.SECONDS)
.writeTimeout(writeTimeout, TimeUnit.SECONDS) .writeTimeout(writeTimeout, TimeUnit.SECONDS)
@ -89,289 +52,199 @@ class DownUtils private constructor() {
.addInterceptor(interceptor ?: LoggingInterceptor()) .addInterceptor(interceptor ?: LoggingInterceptor())
.build() .build()
} }
private var actionGetTotal: (total: Long) -> Unit? = { _ -> }
private var actionProgress: (position: Long) -> Unit? = { _ -> } private var actionGetTotal: (total: Long) -> Unit = {}
private var actionSuccess: (file: File) -> Unit? = { _ -> } private var actionProgress: (position: Long) -> Unit = {}
private var actionCancel: () -> Unit? = { } private var actionSuccess: (file: File) -> Unit = {}
private var actionFail: (msg: String) -> Unit? = { _ -> } private var actionCancel: () -> Unit = {}
private var actionFail: (msg: String) -> Unit = {}
private var downCallBack: DownCallBack? = null
// --- 配置方法 ---
fun setBuffSize(size: Int): DownUtils { this.buffSize = size; return this }
fun setFilePath(path: String): DownUtils { this.filePath = path; return this }
fun setFileName(name: String): DownUtils { this.fileName = name; return this }
fun initUrl(url: String, params: Map<String, String>? = null): DownUtils {
urlBuilder = url.toHttpUrlOrNull()?.newBuilder()
params?.forEach { (k, v) -> urlBuilder?.setQueryParameter(k, v) }
return this
}
fun addHeader(map: Map<String, String>): DownUtils { this.headers.putAll(map); return this }
fun setActionCallBack( fun setActionCallBack(
actionGetTotal: (total: Long) -> Unit, getTotal: (Long) -> Unit,
actionProgress: (position: Long) -> Unit, progress: (Long) -> Unit,
actionSuccess: (file: File) -> Unit, success: (File) -> Unit,
actionFail: (msg: String) -> Unit, fail: (String) -> Unit
): DownUtils { ): DownUtils {
this.actionGetTotal = actionGetTotal this.actionGetTotal = getTotal
this.actionProgress = actionProgress this.actionProgress = progress
this.actionSuccess = actionSuccess this.actionSuccess = success
this.actionFail = actionFail this.actionFail = fail
return this return this
} }
private var downCallBack: DownCallBack? = null
fun setDownCallBack(callBack: DownCallBack): DownUtils { fun setDownCallBack(callBack: DownCallBack): DownUtils {
this.downCallBack = callBack this.downCallBack = callBack
return this return this
} }
fun initUrl(url: String, params: Map<String, String>?): DownUtils {
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>): DownUtils {
requestBuilder.headers(map.toHeaders())
return this
}
private fun checkName(name: String) {
require(name.isNotEmpty()) { "name is empty" }
}
fun down() { fun down() {
if (urlBuilder == null) { val url = urlBuilder?.build() ?: throw IllegalStateException("URL not initialized")
throw IllegalStateException("url not init") doDown(url)
} else {
doDown()
}
} }
private fun doDown() { private fun doDown(url: HttpUrl) {
val startTime = System.currentTimeMillis() val tag = filePath + fileName
Log.i(TAG, "startTime=$startTime") if (isDowning(tag)) return
val url = urlBuilder?.build()
if (url == null) {
doException("url is null")
return
}
if (isDowning(filePath + fileName)) {
return
}
cancelledList.remove(fileName) cancelledList.remove(fileName)
val currentLen = downloadSizeInfo[fileName] ?: 0L // 3. 状态恢复:读取上次进度
if (isCanContinueDownload(url.toUrl(), currentLen)) { var currentLen = downloadSizeInfo[fileName] ?: 0L
requestBuilder.removeHeader("RANGE")
requestBuilder.addHeader("RANGE", "bytes=${currentLen}-")
}
val request = requestBuilder.url(url).tag(filePath + fileName).build()
var `is`: InputStream? = null // 校验如果上次进度不为0验证服务器是否支持断点续传
if (currentLen > 0) {
if (!isCanContinueDownload(url.toUrl(), currentLen)) {
currentLen = 0L // 不支持续传则重置
downloadSizeInfo[fileName] = 0L
}
}
val request = Request.Builder()
.url(url)
.apply {
headers.forEach { (k, v) -> addHeader(k, v) }
if (currentLen > 0) addHeader("RANGE", "bytes=$currentLen-")
}
.tag(tag)
.build()
var inputStream: InputStream? = null
var raf: RandomAccessFile? = null var raf: RandomAccessFile? = null
var file: File? = null var file: File? = null
try { try {
val response = okHttpClient.value.newCall(request).execute() val response = okHttpClient.newCall(request).execute()
val total = response.body.contentLength() if (!response.isSuccessful) throw IOException("Unexpected code $response")
val body = response.body
val total = body.contentLength()
doGetTotal(currentLen + total) doGetTotal(currentLen + total)
val fileDir = File(filePath)
if (!fileDir.exists()) fileDir.mkdirs()
file = File(fileDir, fileName)
inputStream = body.byteStream()
raf = RandomAccessFile(file, "rw")
// 4. 只有响应码为 206 时才 seek否则强制重置到文件开头
if (response.code == 206) {
raf.seek(currentLen)
} else {
raf.seek(0)
currentLen = 0
}
val buf = ByteArray(buffSize) val buf = ByteArray(buffSize)
var len: Int var len: Int
var sum = currentLen
var lastUpdateTime = 0L
file = if (fileName.isEmpty()) { while (inputStream.read(buf).also { len = it } != -1) {
File(filePath)
} else {
val fileDir = File(filePath)
if (!fileDir.exists() || !fileDir.isDirectory) {
fileDir.mkdirs()
}
File(filePath, fileName)
}
`is` = response.body.byteStream()
raf = RandomAccessFile(file, "rw")
raf.seek(currentLen)
var sum: Long = currentLen
while (`is`.read(buf).also { len = it } != -1) {
if (isCancelled()) throw CancellationException() if (isCancelled()) throw CancellationException()
raf.write(buf, 0, len) raf.write(buf, 0, len)
sum += len.toLong() sum += len
downloadSizeInfo[fileName] = sum downloadSizeInfo[fileName] = sum
// 5. 性能节流:限制 UI 刷新频率每100ms一次
val now = System.currentTimeMillis()
if (now - lastUpdateTime > 100L || sum == currentLen + total) {
doProgress(sum) doProgress(sum)
lastUpdateTime = now
}
} }
Log.e(TAG, "download success")
if (!file.exists()) { if (file.exists()) {
throw FileNotFoundException("file create err,not exists")
} else {
doSuccess(file) doSuccess(file)
// 6. 状态清理:成功后移除进度,防止下次下载“续传”到已完成文件的末尾
downloadSizeInfo.remove(fileName)
} }
Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime))
} catch (e: Exception) { } catch (e: Exception) {
if (deleteWhenException && file?.exists() == true) file.delete() Log.e(TAG, "Download failed: ${e.message}")
Log.e(TAG, "download failed : " + e.message) if (e !is CancellationException) {
doException(e.message.toString()) if (deleteWhenException) file?.delete()
doException(e.message ?: "Unknown error")
}
} finally { } finally {
try { inputStream?.close()
`is`?.close()
} catch (e: IOException) {
e.printStackTrace()
}
try {
raf?.close() raf?.close()
} catch (e: IOException) {
e.printStackTrace()
}
} }
} }
private fun isCanContinueDownload(url: URL, start: Long): Boolean { //是否支持断点下载 private fun isCanContinueDownload(url: URL, start: Long): Boolean {
val requestBuilder = Request.Builder() return try {
requestBuilder.addHeader("RANGE", "bytes=$start-") val request = Request.Builder()
requestBuilder.addHeader("Connection", "close") .url(url)
val request: Request = requestBuilder.url(url).head().build() .header("RANGE", "bytes=$start-")
val response: Response = okHttpClient.value.newCall(request).execute() .head()
return if (response.isSuccessful) { .build()
if (response.code == 206) { //支持 okHttpClient.newCall(request).execute().use { it.code == 206 }
response.close() } catch (e: Exception) {
true
} else { //不支持
response.close()
false
}
} else {
response.close()
false false
} }
} }
private fun isDowning(tag: String): Boolean { private fun isDowning(tag: String): Boolean {
for (call in okHttpClient.value.dispatcher.runningCalls()) { return okHttpClient.dispatcher.runningCalls().any { it.request().tag() == tag }
if (call.request().tag() == tag) {
return true
}
}
return false
} }
private fun isCancelled(): Boolean { private fun isCancelled() = cancelledList.contains(fileName)
return cancelledList.contains(fileName)
}
fun cancel() { fun cancel() {
cancel(filePath + fileName) val tag = filePath + fileName
}
fun cancel(tag: String) {
cancelledList.add(fileName) cancelledList.add(fileName)
if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) { okHttpClient.dispatcher.runningCalls().forEach { if (it.request().tag() == tag) it.cancel() }
for (call in okHttpClient.value.dispatcher.runningCalls()) {
if (call.request().tag() == tag) {
call.cancel()
}
}
}
doCancel() 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)
}
downloadSizeInfo.remove(fileName)
}
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) { private fun runOnUiThread(action: () -> Unit) {
if (Looper.myLooper() != Looper.getMainLooper()) { if (Looper.myLooper() == Looper.getMainLooper()) action() else mainHandler.post(action)
if (mainThread == null) {
mainThread = Handler(Looper.getMainLooper())
}
mainThread?.post {
action.invoke()
}
return
}
action.invoke()
} }
private fun doException(err: String) = runOnUiThread {
downCallBack?.fail(err) ?: actionFail.invoke(err)
}
private fun doSuccess(file: File) = runOnUiThread {
downCallBack?.success(file) ?: actionSuccess.invoke(file)
}
private fun doGetTotal(total: Long) = runOnUiThread {
downCallBack?.total(total) ?: actionGetTotal.invoke(total)
}
private fun doProgress(progress: Long) = runOnUiThread {
downCallBack?.progress(progress) ?: actionProgress.invoke(progress)
}
private fun doCancel() = runOnUiThread {
downCallBack?.cancel() ?: actionCancel.invoke()
}
class LoggingInterceptor : Interceptor { class LoggingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request() val request = chain.request()
val startTime = System.nanoTime() val t1 = System.nanoTime()
Log.d( Log.d(TAG, "Sending request ${request.url}")
TAG, String.format( val response = chain.proceed(request)
"Sending request %s on %s%n%s", val t2 = System.nanoTime()
request.url, chain.connection(), request.headers Log.d(TAG, "Received response for ${response.request.url} in ${(t2 - t1) / 1e6}ms")
)
)
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 return response
} }
companion object {
private const val TAG = "LoggingInterceptor"
}
} }
interface DownCallBack { interface DownCallBack {

View File

@ -25,23 +25,27 @@ object UpdateUtils {
} }
var totalProgress = 0L var totalProgress = 0L
DownUtils.getInstance() DownUtils.getInstance()
.setReadTImeOut(10L)
.setDeleteWhenException(false)
.initUrl(url, null) .initUrl(url, null)
.setFilePath(filePath) .setFilePath(filePath)
.setFileName(fileName) .setFileName(fileName)
.setActionCallBack( .setActionCallBack(
{ totalProgress = it }, getTotal = { totalProgress = it },
{ progress = {
val percent = it.toDouble() / totalProgress.toDouble() * 100 val percent = it.toDouble() / totalProgress.toDouble() * 100
val curProgress = percent.toInt() val curProgress = percent.toInt()
onProgress(curProgress) onProgress(curProgress)
}, },
{ success = {
onFinish(true, it.absolutePath) onFinish(true, it.absolutePath)
}, { },
fail = {
if(destination.exists()){
destination.delete()
}
onFinish(false, null) onFinish(false, null)
}).down() }
)
.down()
/* /*
applicationContext?.let { applicationContext?.let {