优化
This commit is contained in:
@@ -13,9 +13,25 @@ import io.legado.app.data.entities.BookSource
|
|||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.ArchiveUtils
|
||||||
import kotlinx.coroutines.*
|
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.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 org.apache.commons.text.similarity.JaccardSimilarity
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
@@ -23,20 +39,20 @@ import java.io.File
|
|||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CopyOnWriteArraySet
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused", "ConstPropertyName")
|
||||||
object BookHelp {
|
object BookHelp {
|
||||||
private val downloadDir: File = appCtx.externalFiles
|
private val downloadDir: File = appCtx.externalFiles
|
||||||
private const val cacheFolderName = "book_cache"
|
private const val cacheFolderName = "book_cache"
|
||||||
private const val cacheImageFolderName = "images"
|
private const val cacheImageFolderName = "images"
|
||||||
private const val cacheEpubFolderName = "epub"
|
private const val cacheEpubFolderName = "epub"
|
||||||
private val downloadImages = CopyOnWriteArraySet<String>()
|
private val downloadImages = ConcurrentHashMap.newKeySet<String>()
|
||||||
|
|
||||||
val cachePath = FileUtils.getPath(downloadDir, cacheFolderName)
|
val cachePath = FileUtils.getPath(downloadDir, cacheFolderName)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package io.legado.app.model
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Build
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.AppLog.putDebug
|
import io.legado.app.constant.AppLog.putDebug
|
||||||
import io.legado.app.constant.PageAnim
|
import io.legado.app.constant.PageAnim
|
||||||
import io.legado.app.data.entities.Book
|
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.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.localBook.EpubFile
|
import io.legado.app.model.localBook.EpubFile
|
||||||
import io.legado.app.model.localBook.PdfFile
|
import io.legado.app.model.localBook.PdfFile
|
||||||
|
import io.legado.app.utils.BitmapCache
|
||||||
import io.legado.app.utils.BitmapUtils
|
import io.legado.app.utils.BitmapUtils
|
||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.SvgUtils
|
import io.legado.app.utils.SvgUtils
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
object ImageProvider {
|
object ImageProvider {
|
||||||
|
|
||||||
@@ -46,7 +50,12 @@ object ImageProvider {
|
|||||||
}
|
}
|
||||||
return AppConfig.bitmapCacheSize * M
|
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) {
|
val bitmapLruCache = object : LruCache<String, Bitmap>(cacheSize) {
|
||||||
|
|
||||||
override fun sizeOf(filePath: String, bitmap: Bitmap): Int {
|
override fun sizeOf(filePath: String, bitmap: Bitmap): Int {
|
||||||
@@ -61,8 +70,8 @@ object ImageProvider {
|
|||||||
) {
|
) {
|
||||||
//错误图片不能释放,占位用,防止一直重复获取图片
|
//错误图片不能释放,占位用,防止一直重复获取图片
|
||||||
if (oldBitmap != errorBitmap) {
|
if (oldBitmap != errorBitmap) {
|
||||||
oldBitmap.recycle()
|
BitmapCache.add(oldBitmap)
|
||||||
triggerRecycled = true
|
//oldBitmap.recycle()
|
||||||
//putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath")
|
//putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath")
|
||||||
//putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes")
|
//putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes")
|
||||||
}
|
}
|
||||||
@@ -166,17 +175,27 @@ object ImageProvider {
|
|||||||
val cacheBitmap = getNotRecycled(vFile.absolutePath)
|
val cacheBitmap = getNotRecycled(vFile.absolutePath)
|
||||||
if (cacheBitmap != null) return cacheBitmap
|
if (cacheBitmap != null) return cacheBitmap
|
||||||
if (height != null && AppConfig.asyncLoadImage && ReadBook.pageAnim() == PageAnim.scrollPageAnim) {
|
if (height != null && AppConfig.asyncLoadImage && ReadBook.pageAnim() == PageAnim.scrollPageAnim) {
|
||||||
|
if (asyncLoadingImages.contains(vFile.absolutePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
asyncLoadingImages.add(vFile.absolutePath)
|
||||||
Coroutine.async {
|
Coroutine.async {
|
||||||
val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
|
BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
|
||||||
?: SvgUtils.createBitmap(vFile.absolutePath, width, height)
|
?: SvgUtils.createBitmap(vFile.absolutePath, width, height)
|
||||||
?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap))
|
?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap))
|
||||||
withContext(Main) {
|
}.onSuccess {
|
||||||
bitmapLruCache.put(vFile.absolutePath, bitmap)
|
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 {
|
}.onError {
|
||||||
//错误图片占位,防止重复获取
|
//错误图片占位,防止重复获取
|
||||||
bitmapLruCache.put(vFile.absolutePath, errorBitmap)
|
bitmapLruCache.put(vFile.absolutePath, errorBitmap)
|
||||||
}.onFinally {
|
}.onFinally {
|
||||||
|
asyncLoadingImages.remove(vFile.absolutePath)
|
||||||
block?.invoke()
|
block?.invoke()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -193,17 +212,4 @@ object ImageProvider {
|
|||||||
}.getOrDefault(errorBitmap)
|
}.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.Canvas
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.os.Build
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.data.entities.Bookmark
|
import io.legado.app.data.entities.Bookmark
|
||||||
import io.legado.app.help.book.isImage
|
import io.legado.app.help.book.isImage
|
||||||
@@ -56,15 +54,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
|||||||
var textPage: TextPage = TextPage()
|
var textPage: TextPage = TextPage()
|
||||||
private set
|
private set
|
||||||
var isMainView = false
|
var isMainView = false
|
||||||
private var drawVisibleImageOnly = false
|
|
||||||
private var cacheIncreased = false
|
|
||||||
private var longScreenshot = 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 reverseStartCursor = false
|
||||||
var reverseEndCursor = false
|
var reverseEndCursor = false
|
||||||
|
|
||||||
@@ -119,8 +109,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
|||||||
}
|
}
|
||||||
canvas.clipRect(visibleRect)
|
canvas.clipRect(visibleRect)
|
||||||
drawPage(canvas)
|
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)
|
canvas.drawText(column.charData, column.start, lineBase, textPaint)
|
||||||
if (column.selected) {
|
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 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(
|
val bitmap = ImageProvider.getImage(
|
||||||
book,
|
book,
|
||||||
column.src,
|
column.src,
|
||||||
(column.end - column.start).toInt(),
|
(column.end - column.start).toInt(),
|
||||||
(lineBottom - lineTop).toInt()
|
(lineBottom - lineTop).toInt()
|
||||||
) {
|
) {
|
||||||
if (!drawVisibleImageOnly && isVisible) {
|
invalidate()
|
||||||
drawVisibleImageOnly = true
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
} ?: return
|
} ?: return
|
||||||
|
|
||||||
val rectF = if (textLine.isImage) {
|
val rectF = if (textLine.isImage) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.CopyOnWriteArraySet
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
private var upTocPool =
|
private var upTocPool =
|
||||||
Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
||||||
private val waitUpTocBooks = arrayListOf<String>()
|
private val waitUpTocBooks = arrayListOf<String>()
|
||||||
private val onUpTocBooks = CopyOnWriteArraySet<String>()
|
private val onUpTocBooks = ConcurrentHashMap.newKeySet<String>()
|
||||||
val onUpBooksLiveData = MutableLiveData<Int>()
|
val onUpBooksLiveData = MutableLiveData<Int>()
|
||||||
private var upTocJob: Job? = null
|
private var upTocJob: Job? = null
|
||||||
private var cacheBookJob: Job? = null
|
private var cacheBookJob: Job? = null
|
||||||
|
|||||||
@@ -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)
|
BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
|
||||||
op.inSampleSize = calculateInSampleSize(op, width, height)
|
op.inSampleSize = calculateInSampleSize(op, width, height)
|
||||||
op.inJustDecodeBounds = false
|
op.inJustDecodeBounds = false
|
||||||
|
BitmapCache.addInBitmapOptions(op)
|
||||||
BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
|
BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user