下载代码优化
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user