[优化] 优化性能
This commit is contained in:
@@ -4,7 +4,6 @@ import io.legado.app.data.AppDatabase
|
|||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
import io.legado.app.data.entities.rule.ExploreKind
|
||||||
import io.legado.app.help.book.isNotShelf
|
|
||||||
import io.legado.app.help.source.exploreKinds
|
import io.legado.app.help.source.exploreKinds
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -26,9 +25,19 @@ class ExploreRepositoryImpl(
|
|||||||
) : ExploreRepository {
|
) : ExploreRepository {
|
||||||
|
|
||||||
override fun getBookshelfItems(): Flow<List<SearchBook>> {
|
override fun getBookshelfItems(): Flow<List<SearchBook>> {
|
||||||
return appDb.bookDao.flowAll().map { books ->
|
return appDb.bookDao.flowBookShelf().map { books ->
|
||||||
books.filterNot { it.isNotShelf }
|
books.filterNot { it.isNotShelf }
|
||||||
.map { it.toSearchBook() }
|
.map {
|
||||||
|
SearchBook(
|
||||||
|
bookUrl = it.bookUrl,
|
||||||
|
name = it.name,
|
||||||
|
author = it.author,
|
||||||
|
originName = it.originName,
|
||||||
|
type = it.type,
|
||||||
|
coverUrl = it.coverUrl,
|
||||||
|
latestChapterTitle = it.latestChapterTitle
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,4 +65,4 @@ class ExploreRepositoryImpl(
|
|||||||
override suspend fun saveSearchBooks(books: List<SearchBook>) {
|
override suspend fun saveSearchBooks(books: List<SearchBook>) {
|
||||||
appDb.searchBookDao.insert(*books.toTypedArray())
|
appDb.searchBookDao.insert(*books.toTypedArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ object AppFreezeMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var registeredReceiver = false
|
private var registeredReceiver = false
|
||||||
|
private var monitorRunnable: Runnable? = null
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
@@ -32,6 +33,10 @@ object AppFreezeMonitor {
|
|||||||
registeredReceiver = false
|
registeredReceiver = false
|
||||||
context.unregisterReceiver(screenStatusReceiver)
|
context.unregisterReceiver(screenStatusReceiver)
|
||||||
}
|
}
|
||||||
|
monitorRunnable?.let {
|
||||||
|
handler.removeCallbacks(it)
|
||||||
|
monitorRunnable = null
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +45,7 @@ object AppFreezeMonitor {
|
|||||||
context.registerReceiver(screenStatusReceiver, screenStatusReceiver.filter)
|
context.registerReceiver(screenStatusReceiver, screenStatusReceiver.filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (monitorRunnable != null) return
|
||||||
var previous = SystemClock.uptimeMillis()
|
var previous = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
val runnable = object : Runnable {
|
val runnable = object : Runnable {
|
||||||
@@ -56,9 +62,12 @@ object AppFreezeMonitor {
|
|||||||
|
|
||||||
if (AppConfig.recordLog) {
|
if (AppConfig.recordLog) {
|
||||||
handler.postDelayed(this, 3000)
|
handler.postDelayed(this, 3000)
|
||||||
|
} else {
|
||||||
|
monitorRunnable = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
monitorRunnable = runnable
|
||||||
handler.postDelayed(runnable, 3000)
|
handler.postDelayed(runnable, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -428,28 +428,87 @@ object BookHelp {
|
|||||||
var ret = true
|
var ret = true
|
||||||
val op = BitmapFactory.Options()
|
val op = BitmapFactory.Options()
|
||||||
op.inJustDecodeBounds = true
|
op.inJustDecodeBounds = true
|
||||||
getContent(book, bookChapter)?.let {
|
forEachImageSrc(book, bookChapter) { src ->
|
||||||
val matcher = AppPattern.imgPattern.matcher(it)
|
val image = getImage(book, src)
|
||||||
while (matcher.find()) {
|
if (!image.exists()) {
|
||||||
val src = matcher.group(1)!!
|
ret = false
|
||||||
val image = getImage(book, src)
|
return@forEachImageSrc
|
||||||
if (!image.exists()) {
|
}
|
||||||
ret = false
|
BitmapFactory.decodeFile(image.absolutePath, op)
|
||||||
continue
|
if (op.outWidth < 1 && op.outHeight < 1) {
|
||||||
}
|
if (SvgUtils.getSize(image.absolutePath) != null) {
|
||||||
BitmapFactory.decodeFile(image.absolutePath, op)
|
return@forEachImageSrc
|
||||||
if (op.outWidth < 1 && op.outHeight < 1) {
|
|
||||||
if (SvgUtils.getSize(image.absolutePath) != null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ret = false
|
|
||||||
image.delete()
|
|
||||||
}
|
}
|
||||||
|
ret = false
|
||||||
|
image.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun forEachImageSrc(
|
||||||
|
book: Book,
|
||||||
|
bookChapter: BookChapter,
|
||||||
|
action: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val file = downloadDir.getFile(
|
||||||
|
cacheFolderName,
|
||||||
|
book.getFolderName(),
|
||||||
|
bookChapter.getFileName()
|
||||||
|
)
|
||||||
|
if (file.exists()) {
|
||||||
|
forEachImageSrc(file, action)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getContent(book, bookChapter)?.let { content ->
|
||||||
|
val matcher = AppPattern.imgPattern.matcher(content)
|
||||||
|
while (matcher.find()) {
|
||||||
|
action(matcher.group(1) ?: continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun forEachImageSrc(file: File, action: (String) -> Unit) {
|
||||||
|
val chars = CharArray(8192)
|
||||||
|
val buffer = StringBuilder()
|
||||||
|
file.bufferedReader().use { reader ->
|
||||||
|
while (true) {
|
||||||
|
val readSize = reader.read(chars)
|
||||||
|
if (readSize < 0) break
|
||||||
|
buffer.append(chars, 0, readSize)
|
||||||
|
consumeImageSrcMatches(buffer, action)
|
||||||
|
trimImageScanBuffer(buffer)
|
||||||
|
}
|
||||||
|
consumeImageSrcMatches(buffer, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun consumeImageSrcMatches(
|
||||||
|
buffer: StringBuilder,
|
||||||
|
action: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val matcher = AppPattern.imgPattern.matcher(buffer)
|
||||||
|
var lastEnd = 0
|
||||||
|
while (matcher.find()) {
|
||||||
|
action(matcher.group(1) ?: continue)
|
||||||
|
lastEnd = matcher.end()
|
||||||
|
}
|
||||||
|
if (lastEnd > 0) {
|
||||||
|
buffer.delete(0, lastEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trimImageScanBuffer(buffer: StringBuilder) {
|
||||||
|
val maxBufferSize = 16 * 1024
|
||||||
|
if (buffer.length <= maxBufferSize) return
|
||||||
|
val partialImgStart = buffer.lastIndexOf("<img")
|
||||||
|
val keepFrom = when {
|
||||||
|
partialImgStart >= 0 -> partialImgStart
|
||||||
|
else -> max(buffer.length - 1024, 0)
|
||||||
|
}
|
||||||
|
buffer.delete(0, keepFrom)
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkImage(bytes: ByteArray): Boolean {
|
private fun checkImage(bytes: ByteArray): Boolean {
|
||||||
val op = BitmapFactory.Options()
|
val op = BitmapFactory.Options()
|
||||||
op.inJustDecodeBounds = true
|
op.inJustDecodeBounds = true
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ object CacheBook {
|
|||||||
private val _downloadErrorFlow =
|
private val _downloadErrorFlow =
|
||||||
MutableStateFlow<Pair<String, Set<Int>>>("" to emptySet())
|
MutableStateFlow<Pair<String, Set<Int>>>("" to emptySet())
|
||||||
val downloadErrorFlow = _downloadErrorFlow.asStateFlow()
|
val downloadErrorFlow = _downloadErrorFlow.asStateFlow()
|
||||||
|
@Volatile
|
||||||
|
private var lastQueueStats = QueueStats(0, 0)
|
||||||
|
|
||||||
val successDownloadSet = ConcurrentHashMap.newKeySet<String>()
|
val successDownloadSet = ConcurrentHashMap.newKeySet<String>()
|
||||||
val errorDownloadMap = ConcurrentHashMap<String, Int>()
|
val errorDownloadMap = ConcurrentHashMap<String, Int>()
|
||||||
@@ -123,7 +125,10 @@ object CacheBook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSummary() {
|
private fun updateSummary() {
|
||||||
_downloadSummaryFlow.value = downloadSummary
|
val stats = collectQueueStats()
|
||||||
|
lastQueueStats = stats
|
||||||
|
_downloadSummaryFlow.value =
|
||||||
|
"正在下载:${stats.downloadingCount}|等待中:${stats.waitingCount}|失败:${errorDownloadMap.size}|成功:${successDownloadSet.size}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -213,12 +218,7 @@ object CacheBook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isRun: Boolean
|
val isRun: Boolean
|
||||||
get() {
|
get() = lastQueueStats.waitingCount > 0 || lastQueueStats.downloadingCount > 0
|
||||||
cacheBookMap.forEach { (_, model) ->
|
|
||||||
if (model.isRun()) return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onTaskQueuesChanged(bookUrl: String) {
|
private fun onTaskQueuesChanged(bookUrl: String) {
|
||||||
updateSummary()
|
updateSummary()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.legado.app.model.CacheBook
|
|||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.service.ExportBookService
|
import io.legado.app.service.ExportBookService
|
||||||
import io.legado.app.ui.config.cacheConfig.CacheConfig
|
import io.legado.app.ui.config.cacheConfig.CacheConfig
|
||||||
|
import io.legado.app.ui.main.bookshelf.toLightBook
|
||||||
import io.legado.app.utils.cnCompare
|
import io.legado.app.utils.cnCompare
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
@@ -219,8 +220,8 @@ class CacheViewModel(
|
|||||||
private fun observeBooks(groupId: Long) {
|
private fun observeBooks(groupId: Long) {
|
||||||
booksJob?.cancel()
|
booksJob?.cancel()
|
||||||
booksJob = viewModelScope.launch {
|
booksJob = viewModelScope.launch {
|
||||||
bookDao.flowByGroup(groupId).map { books ->
|
bookDao.flowBookShelfByGroup(groupId).map { books ->
|
||||||
val booksDownload = books.filter { !it.isAudio }
|
val booksDownload = books.filter { !it.isAudio }.map { it.toLightBook() }
|
||||||
when (cacheConfig.getBookSortByGroupId(groupId)) {
|
when (cacheConfig.getBookSortByGroupId(groupId)) {
|
||||||
1 -> booksDownload.sortedByDescending { it.latestChapterTime }
|
1 -> booksDownload.sortedByDescending { it.latestChapterTime }
|
||||||
2 -> booksDownload.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
|
2 -> booksDownload.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.main.bookshelf
|
|||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -40,3 +41,24 @@ data class BookShelfItem(
|
|||||||
|
|
||||||
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
|
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BookShelfItem.toLightBook() = Book(
|
||||||
|
bookUrl = bookUrl,
|
||||||
|
originName = originName,
|
||||||
|
name = name,
|
||||||
|
author = author,
|
||||||
|
coverUrl = coverUrl,
|
||||||
|
customCoverUrl = customCoverUrl,
|
||||||
|
latestChapterTitle = latestChapterTitle,
|
||||||
|
latestChapterTime = latestChapterTime,
|
||||||
|
lastCheckCount = lastCheckCount,
|
||||||
|
totalChapterNum = totalChapterNum,
|
||||||
|
durChapterTitle = durChapterTitle,
|
||||||
|
durChapterIndex = durChapterIndex,
|
||||||
|
durChapterPos = durChapterPos,
|
||||||
|
durChapterTime = durChapterTime,
|
||||||
|
type = type,
|
||||||
|
group = group,
|
||||||
|
order = order,
|
||||||
|
canUpdate = canUpdate
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user