下载代码优化
This commit is contained in:
3 files changed
+93
-30
No files matched your search
@@ -128,6 +128,10 @@ object CacheBook {
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var lastQueueStats = QueueStats(0, 0)
|
private var lastQueueStats = QueueStats(0, 0)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var lastSummaryUpdateTime = 0L
|
||||||
|
private const val SUMMARY_UPDATE_THROTTLE_MS = 100L
|
||||||
|
|
||||||
private val successDownloadCount = AtomicInteger(0)
|
private val successDownloadCount = AtomicInteger(0)
|
||||||
|
|
||||||
val cacheBookMap: ConcurrentHashMap<String, CacheBookModel>
|
val cacheBookMap: ConcurrentHashMap<String, CacheBookModel>
|
||||||
@@ -177,6 +181,17 @@ object CacheBook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSummary() {
|
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()
|
val stats = collectQueueStats()
|
||||||
lastQueueStats = stats
|
lastQueueStats = stats
|
||||||
_downloadSummaryFlow.value = buildSummary(stats)
|
_downloadSummaryFlow.value = buildSummary(stats)
|
||||||
@@ -482,7 +497,7 @@ object CacheBook {
|
|||||||
} else {
|
} else {
|
||||||
stateStore.clearRuntimeState()
|
stateStore.clearRuntimeState()
|
||||||
}
|
}
|
||||||
updateSummary()
|
updateSummaryImmediate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdownPreservingPaused() {
|
fun shutdownPreservingPaused() {
|
||||||
@@ -502,7 +517,7 @@ object CacheBook {
|
|||||||
pendingRemoveRequests.clear()
|
pendingRemoveRequests.clear()
|
||||||
clearPendingAdmissions()
|
clearPendingAdmissions()
|
||||||
stateStore.clearRuntimeState()
|
stateStore.clearRuntimeState()
|
||||||
updateSummary()
|
updateSummaryImmediate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWorkingState(value: Boolean) {
|
fun setWorkingState(value: Boolean) {
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ class CacheBookModel(
|
|||||||
private val host: Host,
|
private val host: Host,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/** Download timeout: 2 minutes */
|
||||||
|
const val DOWNLOAD_TIMEOUT_MS = 120_000L
|
||||||
|
}
|
||||||
|
|
||||||
interface Host {
|
interface Host {
|
||||||
val stateStore: CacheDownloadStateStore
|
val stateStore: CacheDownloadStateStore
|
||||||
val cacheBookMap: ConcurrentHashMap<String, CacheBookModel>
|
val cacheBookMap: ConcurrentHashMap<String, CacheBookModel>
|
||||||
@@ -58,9 +63,13 @@ class CacheBookModel(
|
|||||||
private val tasks = CompositeCoroutine()
|
private val tasks = CompositeCoroutine()
|
||||||
private val repository = CacheDownloadRepository()
|
private val repository = CacheDownloadRepository()
|
||||||
private val retryCountMap = hashMapOf<Int, Int>()
|
private val retryCountMap = hashMapOf<Int, Int>()
|
||||||
|
@Volatile
|
||||||
private var isStopped = false
|
private var isStopped = false
|
||||||
|
@Volatile
|
||||||
private var waitingRetry = false
|
private var waitingRetry = false
|
||||||
|
@Volatile
|
||||||
private var isLoading = false
|
private var isLoading = false
|
||||||
|
@Volatile
|
||||||
private var isPaused = false
|
private var isPaused = false
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -91,7 +100,6 @@ class CacheBookModel(
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun isDownloading(index: Int): Boolean = onDownloadSet.contains(index)
|
fun isDownloading(index: Int): Boolean = onDownloadSet.contains(index)
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun isPaused(): Boolean = isPaused
|
fun isPaused(): Boolean = isPaused
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -120,12 +128,10 @@ class CacheBookModel(
|
|||||||
return queue.waitingCount() > 0 || onDownloadSet.isNotEmpty() || isLoading || chapterTasks.isNotEmpty()
|
return queue.waitingCount() > 0 || onDownloadSet.isNotEmpty() || isLoading || chapterTasks.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun isStop(): Boolean {
|
fun isStop(): Boolean {
|
||||||
return isStopped || (!isRun() && !waitingRetry)
|
return isStopped || (!isRun() && !waitingRetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun isLoading(): Boolean = isLoading
|
fun isLoading(): Boolean = isLoading
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -498,20 +504,23 @@ class CacheBookModel(
|
|||||||
chapter: BookChapter,
|
chapter: BookChapter,
|
||||||
chapterIndex: Int,
|
chapterIndex: Int,
|
||||||
) {
|
) {
|
||||||
task.onSuccess {
|
task.onSuccess(IO) {
|
||||||
onSuccess(chapter)
|
onSuccess(chapter)
|
||||||
(it as? String)?.let { content ->
|
(it as? String)?.let { content ->
|
||||||
emitPendingReadContent(chapter, content)
|
emitPendingReadContent(chapter, content)
|
||||||
}
|
}
|
||||||
}.onError {
|
}.onError(IO) {
|
||||||
onPreError(chapter, it)
|
onPreError(chapter, it)
|
||||||
delay(1000)
|
try {
|
||||||
onPostError(chapter, it)
|
delay(1000)
|
||||||
|
} finally {
|
||||||
|
onPostError(chapter, it)
|
||||||
|
}
|
||||||
emitPendingReadError(chapter, it)
|
emitPendingReadError(chapter, it)
|
||||||
}.onCancel {
|
}.onCancel(IO) {
|
||||||
onCancel(chapterIndex)
|
onCancel(chapterIndex)
|
||||||
emitPendingReadCanceled(chapter)
|
emitPendingReadCanceled(chapter)
|
||||||
}.onFinally {
|
}.onFinally(IO) {
|
||||||
chapterTasks.remove(chapterIndex)?.let { tasks.delete(it) }
|
chapterTasks.remove(chapterIndex)?.let { tasks.delete(it) }
|
||||||
onFinally()
|
onFinally()
|
||||||
}
|
}
|
||||||
@@ -548,10 +557,27 @@ class CacheBookModel(
|
|||||||
chapter: BookChapter,
|
chapter: BookChapter,
|
||||||
semaphore: Semaphore?,
|
semaphore: Semaphore?,
|
||||||
resetPageOffset: Boolean = false
|
resetPageOffset: Boolean = false
|
||||||
) {
|
): Boolean {
|
||||||
if (!markChapterDownloadStarted(chapter.index)) {
|
if (!markChapterDownloadStarted(chapter.index)) {
|
||||||
markPendingReadRequest(chapter.index, resetPageOffset)
|
// Chapter is already in onDownloadSet. Check if the task is actually alive.
|
||||||
return
|
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(
|
repository.downloadContentTask(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
@@ -562,7 +588,7 @@ class CacheBookModel(
|
|||||||
context = IO,
|
context = IO,
|
||||||
executeContext = IO,
|
executeContext = IO,
|
||||||
semaphore = semaphore
|
semaphore = semaphore
|
||||||
).onSuccess { content ->
|
).timeout(DOWNLOAD_TIMEOUT_MS).onSuccess { content ->
|
||||||
onSuccess(chapter)
|
onSuccess(chapter)
|
||||||
ReadBook.downloadedChapters.add(chapter.index)
|
ReadBook.downloadedChapters.add(chapter.index)
|
||||||
ReadBook.downloadFailChapters.remove(chapter.index)
|
ReadBook.downloadFailChapters.remove(chapter.index)
|
||||||
@@ -580,6 +606,7 @@ class CacheBookModel(
|
|||||||
}.onFinally {
|
}.onFinally {
|
||||||
host.onTaskQueuesChanged(book.bookUrl)
|
host.onTaskQueuesChanged(book.bookUrl)
|
||||||
}.start()
|
}.start()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import io.legado.app.model.webBook.WebBook
|
|||||||
import io.legado.app.service.BaseReadAloudService
|
import io.legado.app.service.BaseReadAloudService
|
||||||
import io.legado.app.service.CacheBookService
|
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.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.ChapterProvider
|
||||||
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
@@ -42,17 +43,17 @@ import kotlinx.coroutines.Dispatchers.Main
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
@@ -750,7 +751,11 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
val book = book ?: return removeLoading(chapter.index)
|
val book = book ?: return removeLoading(chapter.index)
|
||||||
val bookSource = bookSource
|
val bookSource = bookSource
|
||||||
if (bookSource != null) {
|
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 {
|
} else {
|
||||||
val msg = if (book.isLocal) "无内容" else "没有书源"
|
val msg = if (book.isLocal) "无内容" else "没有书源"
|
||||||
contentLoadFinish(
|
contentLoadFinish(
|
||||||
@@ -824,7 +829,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
}
|
}
|
||||||
callBack?.upMenuView()
|
callBack?.upMenuView()
|
||||||
var available = false
|
var available = false
|
||||||
for (page in textChapter.layoutChannel) {
|
collectLayoutPages(textChapter) { page ->
|
||||||
val index = page.index
|
val index = page.index
|
||||||
if (!available && page.containPos(durChapterPos)) {
|
if (!available && page.containPos(durChapterPos)) {
|
||||||
if (upContent) {
|
if (upContent) {
|
||||||
@@ -849,7 +854,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
prevTextChapter = textChapter
|
prevTextChapter = textChapter
|
||||||
}
|
}
|
||||||
textChapter.layoutChannel.receiveAsFlow().collect()
|
collectLayoutPages(textChapter) {}
|
||||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,10 +863,8 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
nextTextChapter = textChapter
|
nextTextChapter = textChapter
|
||||||
}
|
}
|
||||||
for (page in textChapter.layoutChannel) {
|
collectLayoutPages(textChapter) { page ->
|
||||||
if (page.index > 1) {
|
if (page.index > 1) return@collectLayoutPages
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -881,6 +884,26 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
job.start()
|
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(
|
suspend fun contentLoadFinishAwait(
|
||||||
book: Book,
|
book: Book,
|
||||||
chapter: BookChapter,
|
chapter: BookChapter,
|
||||||
@@ -911,7 +934,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
}
|
}
|
||||||
callBack?.upMenuView()
|
callBack?.upMenuView()
|
||||||
var available = false
|
var available = false
|
||||||
for (page in textChapter.layoutChannel) {
|
collectLayoutPages(textChapter) { page ->
|
||||||
val index = page.index
|
val index = page.index
|
||||||
if (!available && page.containPos(durChapterPos)) {
|
if (!available && page.containPos(durChapterPos)) {
|
||||||
if (upContent) {
|
if (upContent) {
|
||||||
@@ -936,7 +959,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
prevTextChapter = textChapter
|
prevTextChapter = textChapter
|
||||||
}
|
}
|
||||||
textChapter.layoutChannel.receiveAsFlow().collect()
|
collectLayoutPages(textChapter) {}
|
||||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,10 +968,8 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
nextTextChapter = textChapter
|
nextTextChapter = textChapter
|
||||||
}
|
}
|
||||||
for (page in textChapter.layoutChannel) {
|
collectLayoutPages(textChapter) { page ->
|
||||||
if (page.index > 1) {
|
if (page.index > 1) return@collectLayoutPages
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user