下载代码优化

This commit is contained in:
HapeLee
2026-05-15 13:04:15 +08:00
parent 7fda288dee
commit af84848a58
3 changed files with 93 additions and 30 deletions
@@ -128,6 +128,10 @@ object CacheBook {
@Volatile
private var lastQueueStats = QueueStats(0, 0)
@Volatile
private var lastSummaryUpdateTime = 0L
private const val SUMMARY_UPDATE_THROTTLE_MS = 100L
private val successDownloadCount = AtomicInteger(0)
val cacheBookMap: ConcurrentHashMap<String, CacheBookModel>
@@ -177,6 +181,17 @@ object CacheBook {
}
private fun updateSummary() {
val now = System.currentTimeMillis()
if (now - lastSummaryUpdateTime < SUMMARY_UPDATE_THROTTLE_MS) {
return
}
lastSummaryUpdateTime = now
val stats = collectQueueStats()
lastQueueStats = stats
_downloadSummaryFlow.value = buildSummary(stats)
}
private fun updateSummaryImmediate() {
val stats = collectQueueStats()
lastQueueStats = stats
_downloadSummaryFlow.value = buildSummary(stats)
@@ -482,7 +497,7 @@ object CacheBook {
} else {
stateStore.clearRuntimeState()
}
updateSummary()
updateSummaryImmediate()
}
fun shutdownPreservingPaused() {
@@ -502,7 +517,7 @@ object CacheBook {
pendingRemoveRequests.clear()
clearPendingAdmissions()
stateStore.clearRuntimeState()
updateSummary()
updateSummaryImmediate()
}
fun setWorkingState(value: Boolean) {
@@ -29,6 +29,11 @@ class CacheBookModel(
private val host: Host,
) {
private companion object {
/** Download timeout: 2 minutes */
const val DOWNLOAD_TIMEOUT_MS = 120_000L
}
interface Host {
val stateStore: CacheDownloadStateStore
val cacheBookMap: ConcurrentHashMap<String, CacheBookModel>
@@ -58,9 +63,13 @@ class CacheBookModel(
private val tasks = CompositeCoroutine()
private val repository = CacheDownloadRepository()
private val retryCountMap = hashMapOf<Int, Int>()
@Volatile
private var isStopped = false
@Volatile
private var waitingRetry = false
@Volatile
private var isLoading = false
@Volatile
private var isPaused = false
@Synchronized
@@ -91,7 +100,6 @@ class CacheBookModel(
@Synchronized
fun isDownloading(index: Int): Boolean = onDownloadSet.contains(index)
@Synchronized
fun isPaused(): Boolean = isPaused
@Synchronized
@@ -120,12 +128,10 @@ class CacheBookModel(
return queue.waitingCount() > 0 || onDownloadSet.isNotEmpty() || isLoading || chapterTasks.isNotEmpty()
}
@Synchronized
fun isStop(): Boolean {
return isStopped || (!isRun() && !waitingRetry)
}
@Synchronized
fun isLoading(): Boolean = isLoading
@Synchronized
@@ -498,20 +504,23 @@ class CacheBookModel(
chapter: BookChapter,
chapterIndex: Int,
) {
task.onSuccess {
task.onSuccess(IO) {
onSuccess(chapter)
(it as? String)?.let { content ->
emitPendingReadContent(chapter, content)
}
}.onError {
}.onError(IO) {
onPreError(chapter, it)
delay(1000)
onPostError(chapter, it)
try {
delay(1000)
} finally {
onPostError(chapter, it)
}
emitPendingReadError(chapter, it)
}.onCancel {
}.onCancel(IO) {
onCancel(chapterIndex)
emitPendingReadCanceled(chapter)
}.onFinally {
}.onFinally(IO) {
chapterTasks.remove(chapterIndex)?.let { tasks.delete(it) }
onFinally()
}
@@ -548,10 +557,27 @@ class CacheBookModel(
chapter: BookChapter,
semaphore: Semaphore?,
resetPageOffset: Boolean = false
) {
): Boolean {
if (!markChapterDownloadStarted(chapter.index)) {
markPendingReadRequest(chapter.index, resetPageOffset)
return
// Chapter is already in onDownloadSet. Check if the task is actually alive.
val hasLiveTask = synchronized(this) {
chapterTasks.containsKey(chapter.index)
}
if (!hasLiveTask) {
// Stale entry: onDownloadSet has the index but no live task.
// Clean up and retry.
synchronized(this) {
onDownloadSet.remove(chapter.index)
}
notifyDownloadSetChanged()
if (!markChapterDownloadStarted(chapter.index)) {
markPendingReadRequest(chapter.index, resetPageOffset)
return false
}
} else {
markPendingReadRequest(chapter.index, resetPageOffset)
return true
}
}
repository.downloadContentTask(
scope = scope,
@@ -562,7 +588,7 @@ class CacheBookModel(
context = IO,
executeContext = IO,
semaphore = semaphore
).onSuccess { content ->
).timeout(DOWNLOAD_TIMEOUT_MS).onSuccess { content ->
onSuccess(chapter)
ReadBook.downloadedChapters.add(chapter.index)
ReadBook.downloadFailChapters.remove(chapter.index)
@@ -580,6 +606,7 @@ class CacheBookModel(
}.onFinally {
host.onTaskQueuesChanged(book.bookUrl)
}.start()
return true
}
@Synchronized
@@ -30,6 +30,7 @@ import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.CacheBookService
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
import io.legado.app.utils.postEvent
@@ -42,17 +43,17 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import splitties.init.appCtx
@@ -750,7 +751,11 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
val book = book ?: return removeLoading(chapter.index)
val bookSource = bookSource
if (bookSource != null) {
CacheBook.getOrCreate(bookSource, book).download(scope, chapter, semaphore)
val started =
CacheBook.getOrCreate(bookSource, book).download(scope, chapter, semaphore)
if (!started) {
removeLoading(chapter.index)
}
} else {
val msg = if (book.isLocal) "无内容" else "没有书源"
contentLoadFinish(
@@ -824,7 +829,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
}
callBack?.upMenuView()
var available = false
for (page in textChapter.layoutChannel) {
collectLayoutPages(textChapter) { page ->
val index = page.index
if (!available && page.containPos(durChapterPos)) {
if (upContent) {
@@ -849,7 +854,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
ensureActive()
prevTextChapter = textChapter
}
textChapter.layoutChannel.receiveAsFlow().collect()
collectLayoutPages(textChapter) {}
if (upContent) callBack?.upContent(offset, resetPageOffset)
}
@@ -858,10 +863,8 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
ensureActive()
nextTextChapter = textChapter
}
for (page in textChapter.layoutChannel) {
if (page.index > 1) {
continue
}
collectLayoutPages(textChapter) { page ->
if (page.index > 1) return@collectLayoutPages
if (upContent) callBack?.upContent(offset, resetPageOffset)
}
}
@@ -881,6 +884,26 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
job.start()
}
/**
* Safely collect pages from a TextChapterLayout's channel with timeout protection.
* Prevents indefinite blocking if the layout job hangs without closing the channel.
*/
private suspend fun collectLayoutPages(
textChapter: TextChapter,
onPage: (TextPage) -> Unit,
) {
try {
withTimeout(30_000L) {
for (page in textChapter.layoutChannel) {
ensureActive()
onPage(page)
}
}
} catch (_: TimeoutCancellationException) {
AppLog.put("Layout channel timeout for chapter ${textChapter.chapter.index}")
}
}
suspend fun contentLoadFinishAwait(
book: Book,
chapter: BookChapter,
@@ -911,7 +934,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
}
callBack?.upMenuView()
var available = false
for (page in textChapter.layoutChannel) {
collectLayoutPages(textChapter) { page ->
val index = page.index
if (!available && page.containPos(durChapterPos)) {
if (upContent) {
@@ -936,7 +959,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
withContext(Main) {
prevTextChapter = textChapter
}
textChapter.layoutChannel.receiveAsFlow().collect()
collectLayoutPages(textChapter) {}
if (upContent) callBack?.upContent(offset, resetPageOffset)
}
@@ -945,10 +968,8 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
withContext(Main) {
nextTextChapter = textChapter
}
for (page in textChapter.layoutChannel) {
if (page.index > 1) {
continue
}
collectLayoutPages(textChapter) { page ->
if (page.index > 1) return@collectLayoutPages
if (upContent) callBack?.upContent(offset, resetPageOffset)
}
}