mirror of
https://github.com/gedoor/legado.git
synced 2024-07-06 23:47:49 +08:00
优化
This commit is contained in:
parent
d96de11b12
commit
e895223638
@ -13,9 +13,25 @@ import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.*
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.ImageUtils
|
||||
import io.legado.app.utils.MD5Utils
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import io.legado.app.utils.StringUtils
|
||||
import io.legado.app.utils.SvgUtils
|
||||
import io.legado.app.utils.UrlUtil
|
||||
import io.legado.app.utils.exists
|
||||
import io.legado.app.utils.externalFiles
|
||||
import io.legado.app.utils.getFile
|
||||
import io.legado.app.utils.isContentScheme
|
||||
import io.legado.app.utils.postEvent
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.text.similarity.JaccardSimilarity
|
||||
import splitties.init.appCtx
|
||||
import java.io.ByteArrayInputStream
|
||||
@ -23,20 +39,20 @@ import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.Pattern
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("unused")
|
||||
@Suppress("unused", "ConstPropertyName")
|
||||
object BookHelp {
|
||||
private val downloadDir: File = appCtx.externalFiles
|
||||
private const val cacheFolderName = "book_cache"
|
||||
private const val cacheImageFolderName = "images"
|
||||
private const val cacheEpubFolderName = "epub"
|
||||
private val downloadImages = CopyOnWriteArraySet<String>()
|
||||
private val downloadImages = ConcurrentHashMap.newKeySet<String>()
|
||||
|
||||
val cachePath = FileUtils.getPath(downloadDir, cacheFolderName)
|
||||
|
||||
|
@ -2,9 +2,11 @@ package io.legado.app.model
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.util.Size
|
||||
import androidx.collection.LruCache
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppLog.putDebug
|
||||
import io.legado.app.constant.PageAnim
|
||||
import io.legado.app.data.entities.Book
|
||||
@ -17,16 +19,18 @@ import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.localBook.EpubFile
|
||||
import io.legado.app.model.localBook.PdfFile
|
||||
import io.legado.app.utils.BitmapCache
|
||||
import io.legado.app.utils.BitmapUtils
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.SvgUtils
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.min
|
||||
|
||||
object ImageProvider {
|
||||
|
||||
@ -46,7 +50,12 @@ object ImageProvider {
|
||||
}
|
||||
return AppConfig.bitmapCacheSize * M
|
||||
}
|
||||
var triggerRecycled = false
|
||||
private val maxCacheSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
|
||||
min(128 * M, Runtime.getRuntime().maxMemory().toInt())
|
||||
} else {
|
||||
256 * M
|
||||
}
|
||||
private val asyncLoadingImages = ConcurrentHashMap.newKeySet<String>()
|
||||
val bitmapLruCache = object : LruCache<String, Bitmap>(cacheSize) {
|
||||
|
||||
override fun sizeOf(filePath: String, bitmap: Bitmap): Int {
|
||||
@ -61,8 +70,8 @@ object ImageProvider {
|
||||
) {
|
||||
//错误图片不能释放,占位用,防止一直重复获取图片
|
||||
if (oldBitmap != errorBitmap) {
|
||||
oldBitmap.recycle()
|
||||
triggerRecycled = true
|
||||
BitmapCache.add(oldBitmap)
|
||||
//oldBitmap.recycle()
|
||||
//putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath")
|
||||
//putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes")
|
||||
}
|
||||
@ -166,17 +175,27 @@ object ImageProvider {
|
||||
val cacheBitmap = getNotRecycled(vFile.absolutePath)
|
||||
if (cacheBitmap != null) return cacheBitmap
|
||||
if (height != null && AppConfig.asyncLoadImage && ReadBook.pageAnim() == PageAnim.scrollPageAnim) {
|
||||
if (asyncLoadingImages.contains(vFile.absolutePath)) {
|
||||
return null
|
||||
}
|
||||
asyncLoadingImages.add(vFile.absolutePath)
|
||||
Coroutine.async {
|
||||
val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
|
||||
BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
|
||||
?: SvgUtils.createBitmap(vFile.absolutePath, width, height)
|
||||
?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap))
|
||||
withContext(Main) {
|
||||
bitmapLruCache.put(vFile.absolutePath, bitmap)
|
||||
}.onSuccess {
|
||||
bitmapLruCache.run {
|
||||
if (maxSize() < maxCacheSize && size() + it.byteCount > maxSize() && putCount() - evictionCount() < 5) {
|
||||
resize(min(maxCacheSize, maxSize() + it.byteCount))
|
||||
AppLog.put("图片缓存太小,自动扩增至${(maxSize() / M)}MB。")
|
||||
}
|
||||
}
|
||||
bitmapLruCache.put(vFile.absolutePath, it)
|
||||
}.onError {
|
||||
//错误图片占位,防止重复获取
|
||||
bitmapLruCache.put(vFile.absolutePath, errorBitmap)
|
||||
}.onFinally {
|
||||
asyncLoadingImages.remove(vFile.absolutePath)
|
||||
block?.invoke()
|
||||
}
|
||||
return null
|
||||
@ -193,17 +212,4 @@ object ImageProvider {
|
||||
}.getOrDefault(errorBitmap)
|
||||
}
|
||||
|
||||
fun isImageAlive(book: Book, src: String): Boolean {
|
||||
val vFile = BookHelp.getImage(book, src)
|
||||
if (!vFile.exists()) return true // 使用 errorBitmap
|
||||
val cacheBitmap = bitmapLruCache.get(vFile.absolutePath)
|
||||
return cacheBitmap != null
|
||||
}
|
||||
|
||||
fun isTriggerRecycled(): Boolean {
|
||||
val tmp = triggerRecycled
|
||||
triggerRecycled = false
|
||||
return tmp
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,12 +4,10 @@ import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.help.book.isImage
|
||||
@ -56,15 +54,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
||||
var textPage: TextPage = TextPage()
|
||||
private set
|
||||
var isMainView = false
|
||||
private var drawVisibleImageOnly = false
|
||||
private var cacheIncreased = false
|
||||
private var longScreenshot = false
|
||||
private val increaseSize = 8 * 1024 * 1024
|
||||
private val maxCacheSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
|
||||
min(128 * 1024 * 1024, Runtime.getRuntime().maxMemory())
|
||||
} else {
|
||||
256 * 1024 * 1024
|
||||
}
|
||||
var reverseStartCursor = false
|
||||
var reverseEndCursor = false
|
||||
|
||||
@ -119,8 +109,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
||||
}
|
||||
canvas.clipRect(visibleRect)
|
||||
drawPage(canvas)
|
||||
drawVisibleImageOnly = false
|
||||
cacheIncreased = false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -212,7 +200,13 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
||||
}
|
||||
canvas.drawText(column.charData, column.start, lineBase, textPaint)
|
||||
if (column.selected) {
|
||||
canvas.drawRect(column.start, lineTop, column.end, lineBottom, selectedPaint)
|
||||
canvas.drawRect(
|
||||
column.start,
|
||||
lineTop,
|
||||
column.end,
|
||||
lineBottom,
|
||||
selectedPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,37 +230,14 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
||||
) {
|
||||
|
||||
val book = ReadBook.book ?: return
|
||||
val isVisible = when {
|
||||
lineTop > 0 -> lineTop < height
|
||||
lineTop < 0 -> lineBottom > 0
|
||||
else -> true
|
||||
}
|
||||
if (drawVisibleImageOnly && !isVisible) {
|
||||
return
|
||||
}
|
||||
if (drawVisibleImageOnly &&
|
||||
!cacheIncreased &&
|
||||
ImageProvider.isTriggerRecycled() &&
|
||||
!ImageProvider.isImageAlive(book, column.src)
|
||||
) {
|
||||
val newSize = ImageProvider.bitmapLruCache.maxSize() + increaseSize
|
||||
if (newSize < maxCacheSize) {
|
||||
ImageProvider.bitmapLruCache.resize(newSize)
|
||||
AppLog.put("图片缓存不够大,自动扩增至${(newSize / 1024 / 1024)}MB。")
|
||||
cacheIncreased = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val bitmap = ImageProvider.getImage(
|
||||
book,
|
||||
column.src,
|
||||
(column.end - column.start).toInt(),
|
||||
(lineBottom - lineTop).toInt()
|
||||
) {
|
||||
if (!drawVisibleImageOnly && isVisible) {
|
||||
drawVisibleImageOnly = true
|
||||
invalidate()
|
||||
}
|
||||
invalidate()
|
||||
} ?: return
|
||||
|
||||
val rectF = if (textLine.isImage) {
|
||||
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.min
|
||||
|
||||
@ -40,7 +40,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
|
||||
private var upTocPool =
|
||||
Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
||||
private val waitUpTocBooks = arrayListOf<String>()
|
||||
private val onUpTocBooks = CopyOnWriteArraySet<String>()
|
||||
private val onUpTocBooks = ConcurrentHashMap.newKeySet<String>()
|
||||
val onUpBooksLiveData = MutableLiveData<Int>()
|
||||
private var upTocJob: Job? = null
|
||||
private var cacheBookJob: Job? = null
|
||||
|
77
app/src/main/java/io/legado/app/utils/BitmapCache.kt
Normal file
77
app/src/main/java/io/legado/app/utils/BitmapCache.kt
Normal file
@ -0,0 +1,77 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import java.lang.ref.SoftReference
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object BitmapCache {
|
||||
|
||||
private val reusableBitmaps: MutableSet<SoftReference<Bitmap>> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
fun add(bitmap: Bitmap) {
|
||||
reusableBitmaps.add(SoftReference(bitmap))
|
||||
}
|
||||
|
||||
fun addInBitmapOptions(options: BitmapFactory.Options) {
|
||||
// inBitmap only works with mutable bitmaps, so force the decoder to
|
||||
// return mutable bitmaps.
|
||||
options.inMutable = true
|
||||
|
||||
// Try to find a bitmap to use for inBitmap.
|
||||
getBitmapFromReusableSet(options)?.also { inBitmap ->
|
||||
// If a suitable bitmap has been found, set it as the value of
|
||||
// inBitmap.
|
||||
options.inBitmap = inBitmap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getBitmapFromReusableSet(options: BitmapFactory.Options): Bitmap? {
|
||||
if (reusableBitmaps.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val iterator = reusableBitmaps.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val item = iterator.next().get() ?: continue
|
||||
if (item.isMutable) {
|
||||
// Check to see it the item can be used for inBitmap.
|
||||
if (canUseForInBitmap(item, options)) {
|
||||
// Remove from reusable set so it can't be used again.
|
||||
iterator.remove()
|
||||
return item
|
||||
}
|
||||
} else {
|
||||
// Remove from the set if the reference has been cleared.
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun canUseForInBitmap(
|
||||
candidate: Bitmap,
|
||||
targetOptions: BitmapFactory.Options
|
||||
): Boolean {
|
||||
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
|
||||
// the new bitmap is smaller than the reusable bitmap candidate
|
||||
// allocation byte count.
|
||||
val width: Int = targetOptions.outWidth / targetOptions.inSampleSize
|
||||
val height: Int = targetOptions.outHeight / targetOptions.inSampleSize
|
||||
val byteCount: Int = width * height * getBytesPerPixel(candidate.config)
|
||||
return byteCount <= candidate.allocationByteCount
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to return the byte usage per pixel of a bitmap based on its configuration.
|
||||
*/
|
||||
private fun getBytesPerPixel(config: Bitmap.Config): Int {
|
||||
return when (config) {
|
||||
Bitmap.Config.ARGB_8888 -> 4
|
||||
Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2
|
||||
Bitmap.Config.ALPHA_8 -> 1
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -34,6 +34,7 @@ object BitmapUtils {
|
||||
BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
|
||||
op.inSampleSize = calculateInSampleSize(op, width, height)
|
||||
op.inJustDecodeBounds = false
|
||||
BitmapCache.addInBitmapOptions(op)
|
||||
BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user