下载优化

This commit is contained in:
HapeLee
2026-05-15 03:04:07 +08:00
parent 22cb773ada
commit cfd21da161
5 changed files with 65 additions and 33 deletions
@@ -17,6 +17,7 @@ import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.onEachParallel import io.legado.app.utils.onEachParallel
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
@@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@@ -180,16 +182,14 @@ object CacheBook {
_downloadSummaryFlow.value = buildSummary(stats) _downloadSummaryFlow.value = buildSummary(stats)
} }
@Synchronized suspend fun getOrCreate(bookUrl: String): CacheBookModel? = withContext(Dispatchers.IO) {
fun getOrCreate(bookUrl: String): CacheBookModel? { val book = appDb.bookDao.getBook(bookUrl) ?: return@withContext null
val book = appDb.bookDao.getBook(bookUrl) ?: return null val source = appDb.bookSourceDao.getBookSource(book.origin) ?: return@withContext null
val source = appDb.bookSourceDao.getBookSource(book.origin) ?: return null getOrCreate(source, book)
return getOrCreate(source, book)
} }
@Synchronized @Synchronized
fun getOrCreate(bookSource: BookSource, book: Book): CacheBookModel { fun getOrCreate(bookSource: BookSource, book: Book): CacheBookModel {
updateBookSource(bookSource)
cacheBookMap[book.bookUrl]?.let { model -> cacheBookMap[book.bookUrl]?.let { model ->
model.bookSource = bookSource model.bookSource = bookSource
model.book = book model.book = book
@@ -202,14 +202,16 @@ object CacheBook {
} }
private fun updateBookSource(newBookSource: BookSource) { private fun updateBookSource(newBookSource: BookSource) {
cacheBookMap.forEach { (_, model) -> // 只有在必要时才更新,且避免在 getOrCreate 中高频调用
if (model.bookSource.bookSourceUrl == newBookSource.bookSourceUrl) { val sourceUrl = newBookSource.bookSourceUrl
cacheBookMap.values.forEach { model ->
if (model.bookSource.bookSourceUrl == sourceUrl && model.bookSource != newBookSource) {
model.bookSource = newBookSource model.bookSource = newBookSource
} }
} }
} }
fun start(context: Context, book: Book, selectedIndices: List<Int>) { suspend fun start(context: Context, book: Book, selectedIndices: List<Int>) {
start( start(
context = context, context = context,
request = CacheDownloadRequest( request = CacheDownloadRequest(
@@ -220,7 +222,7 @@ object CacheBook {
) )
} }
fun start(context: Context, book: Book, startIndex: Int, endIndex: Int) { suspend fun start(context: Context, book: Book, startIndex: Int, endIndex: Int) {
start( start(
context = context, context = context,
request = CacheDownloadRequest( request = CacheDownloadRequest(
@@ -241,19 +243,28 @@ object CacheBook {
} }
} }
fun start(context: Context, requests: List<CacheDownloadRequest>) { suspend fun start(context: Context, requests: List<CacheDownloadRequest>) = withContext(Dispatchers.IO) {
requests.asSequence() val validRequests = requests.filter { it.hasValidSelection() }
.filter { it.hasValidSelection() } if (validRequests.isEmpty()) return@withContext
.filter { request ->
appDb.bookDao.getBook(request.bookUrl)?.isLocal != true val urls = validRequests.map { it.bookUrl }.toSet()
} val localBookUrls = appDb.bookDao.getCacheableBooks(urls)
.forEach { request -> .filter { it.isLocal }
isPaused = false .map { it.bookUrl }
startCacheBookService(context) { .toSet()
action = IntentAction.start
putRequestExtras(request) val finalRequests = validRequests.filterNot { it.bookUrl in localBookUrls }
} if (finalRequests.isEmpty()) return@withContext
isPaused = false
// 如果请求较多,可以通过 Intent 传递一个特殊的标志让 Service 自己去检查队列,
// 或者分批发送。这里我们先简单处理,但确保不在主线程做数据库查询。
finalRequests.forEach { request ->
startCacheBookService(context) {
action = IntentAction.start
putRequestExtras(request)
} }
}
} }
private fun android.content.Intent.putRequestExtras(request: CacheDownloadRequest) { private fun android.content.Intent.putRequestExtras(request: CacheDownloadRequest) {
@@ -116,12 +116,21 @@ class CacheDownloadStateStore {
} }
private fun CacheDownloadState.recalculate(): CacheDownloadState { private fun CacheDownloadState.recalculate(): CacheDownloadState {
val totalWaiting = books.values.sumOf { it.waitingCount } var totalWaiting = 0
val totalRunning = books.values.sumOf { it.runningIndices.size } var totalRunning = 0
val totalPaused = books.values.sumOf { it.pausedIndices.size } var totalPaused = 0
val totalFailure = books.values.sumOf { it.failedIndices.size } + var totalFailure = 0
books.values.count { it.failureMessage != null } var totalSuccess = 0
val totalSuccess = books.values.sumOf { it.successCount }
books.values.forEach { bookState ->
totalWaiting += bookState.waitingCount
totalRunning += bookState.runningIndices.size
totalPaused += bookState.pausedIndices.size
totalFailure += bookState.failedIndices.size
if (bookState.failureMessage != null) totalFailure++
totalSuccess += bookState.successCount
}
return copy( return copy(
isRunning = totalWaiting > totalPaused || totalRunning > 0, isRunning = totalWaiting > totalPaused || totalRunning > 0,
totalWaiting = totalWaiting, totalWaiting = totalWaiting,
@@ -150,7 +150,7 @@ class CacheBookService : BaseService() {
serviceCommandMutex.withLock { serviceCommandMutex.withLock {
CacheBook.pauseAllFromService() CacheBook.pauseAllFromService()
notificationContent = CacheBook.downloadSummary notificationContent = CacheBook.downloadSummary
upCacheBookNotification() upCacheBookNotification(force = true)
} }
} }
} }
@@ -160,7 +160,7 @@ class CacheBookService : BaseService() {
CacheBook.resumeFromService() CacheBook.resumeFromService()
ensureDownloadJob() ensureDownloadJob()
notificationContent = CacheBook.downloadSummary notificationContent = CacheBook.downloadSummary
upCacheBookNotification() upCacheBookNotification(force = true)
} }
} }
} }
@@ -364,7 +364,8 @@ class CacheBookService : BaseService() {
} }
notificationContent = CacheBook.downloadSummary notificationContent = CacheBook.downloadSummary
upCacheBookNotification() // 移除这里的直接调用,依靠 onCreate 的循环更新
// upCacheBookNotification()
} }
private fun AdmissionRequest.isCurrent(): Boolean { private fun AdmissionRequest.isCurrent(): Boolean {
@@ -521,7 +522,13 @@ class CacheBookService : BaseService() {
} }
} }
private fun upCacheBookNotification() { private var lastNotificationTime = 0L
private fun upCacheBookNotification(force: Boolean = false) {
val now = System.currentTimeMillis()
if (!force && now - lastNotificationTime < 500L) return
lastNotificationTime = now
val total = CacheBook.totalCount val total = CacheBook.totalCount
val progress = CacheBook.completedCount val progress = CacheBook.completedCount
val pendingBookCount = synchronized(admissionQueue) { admissionQueue.size } val pendingBookCount = synchronized(admissionQueue) { admissionQueue.size }
@@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
@@ -48,6 +49,7 @@ import io.legado.app.utils.isTv
import io.legado.app.utils.setLightStatusBar import io.legado.app.utils.setLightStatusBar
import io.legado.app.utils.setNavigationBarColorAuto import io.legado.app.utils.setNavigationBarColorAuto
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
import kotlinx.coroutines.launch
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.themeColor import io.legado.app.utils.themeColor
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -304,7 +306,9 @@ abstract class BaseReadBookActivity :
val end = editEnd.text!!.toString().let { val end = editEnd.text!!.toString().let {
if (it.isEmpty()) book.totalChapterNum else it.toInt() if (it.isEmpty()) book.totalChapterNum else it.toInt()
} }
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1) lifecycleScope.launch {
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
}
} }
} }
cancelButton() cancelButton()
@@ -38,6 +38,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppFloatingActionButton import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet