Horis 2 gadi atpakaļ
vecāks
revīzija
63602a51bf

+ 21 - 5
app/src/main/java/io/legado/app/help/book/BookHelp.kt

@@ -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)
 

+ 26 - 20
app/src/main/java/io/legado/app/model/ImageProvider.kt

@@ -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
-    }
-
 }

+ 9 - 38
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -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) {

+ 2 - 2
app/src/main/java/io/legado/app/ui/main/MainViewModel.kt

@@ -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 - 0
app/src/main/java/io/legado/app/utils/BitmapCache.kt

@@ -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
+        }
+    }
+
+}

+ 1 - 0
app/src/main/java/io/legado/app/utils/BitmapUtils.kt

@@ -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)
         }
     }