فهرست منبع

修复正则替换超时任务导致内存溢出

5151561 3 ماه پیش
والد
کامیت
4914622915
1فایلهای تغییر یافته به همراه68 افزوده شده و 43 حذف شده
  1. 68 43
      app/src/main/java/io/legado/app/utils/RegexExtensions.kt

+ 68 - 43
app/src/main/java/io/legado/app/utils/RegexExtensions.kt

@@ -1,6 +1,5 @@
 package io.legado.app.utils
 
-import androidx.core.os.postDelayed
 import com.script.ScriptBindings
 import com.script.rhino.RhinoScriptEngine
 import io.legado.app.data.appDb
@@ -8,15 +7,13 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.exception.RegexTimeoutException
 import io.legado.app.help.CrashHandler
-import io.legado.app.help.coroutine.Coroutine
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.runInterruptible
+import kotlinx.coroutines.withTimeout
 import splitties.init.appCtx
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-
-private val handler by lazy { buildMainHandler() }
 
 /**
  * 带有超时检测的正则替换
@@ -39,48 +36,76 @@ fun CharSequence.replace(
         null
     }
     return runBlocking {
-        suspendCancellableCoroutine { block ->
-            val coroutine = Coroutine.async(executeContext = IO) {
-                try {
+        try {
+            withTimeout(timeout) {
+                val timeoutContext = currentCoroutineContext()
+                runInterruptible(IO) {
                     val pattern = regex.toPattern()
-                    val matcher = pattern.matcher(charSequence)
+                    val matcher = pattern.matcher(InterruptibleCharSequence(charSequence))
                     val stringBuffer = StringBuffer()
-                    while (matcher.find()) {
-                        if (isJs) {
-                            val jsResult = RhinoScriptEngine.run {
-                                val bindings = ScriptBindings()
-                                bindings["result"] = matcher.group()
-                                bindings["chapter"] = chapter
-                                bindings["book"] = book
-                                eval(replacement1, bindings)
-                            }.toString()
-                            val quotedResult = jsResult.quoteReplacementJs()
-                            matcher.appendReplacement(stringBuffer, quotedResult)
-                        } else {
-                            matcher.appendReplacement(stringBuffer, replacement1)
-                        }
-                    }
-                    matcher.appendTail(stringBuffer)
-                    block.resume(stringBuffer.toString())
-                } catch (e: Exception) {
-                    block.resumeWithException(e)
-                }
-            }
-            handler.postDelayed(timeout) {
-                if (coroutine.isActive) {
-                    val timeoutMsg =
-                        "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:$charSequence"
-                    val exception = RegexTimeoutException(timeoutMsg)
-                    block.cancel(exception)
-                    appCtx.longToastOnUi(timeoutMsg)
-                    CrashHandler.saveCrashInfo2File(exception)
-                    handler.postDelayed(3000) {
-                        if (coroutine.isActive) {
-                            appCtx.restart()
+                    try {
+                        while (matcher.find()) {
+                            if (isJs) {
+                                val jsResult = RhinoScriptEngine.run {
+                                    val bindings = ScriptBindings()
+                                    bindings["result"] = matcher.group()
+                                    bindings["chapter"] = chapter
+                                    bindings["book"] = book
+                                    eval(
+                                        replacement1,
+                                        getRuntimeScope(bindings),
+                                        timeoutContext,
+                                    )
+                                }.toString()
+                                val quotedResult = jsResult.quoteReplacementJs()
+                                matcher.appendReplacement(stringBuffer, quotedResult)
+                            } else {
+                                matcher.appendReplacement(stringBuffer, replacement1)
+                            }
                         }
+                        matcher.appendTail(stringBuffer)
+                    } catch (_: RegexInterruptedException) {
+                        throw InterruptedException()
                     }
+                    stringBuffer.toString()
                 }
             }
+        } catch (_: TimeoutCancellationException) {
+            val timeoutMsg = "替换超时\n替换规则$regex\n替换内容:$charSequence"
+            val exception = RegexTimeoutException(timeoutMsg)
+            appCtx.longToastOnUi(timeoutMsg)
+            CrashHandler.saveCrashInfo2File(exception)
+            throw exception
+        }
+    }
+}
+
+/**
+ * Checks the worker thread's interrupted state while java.util.regex reads its input.
+ */
+private class InterruptibleCharSequence(private val delegate: CharSequence) : CharSequence {
+
+    private var counter = 0
+
+    override val length: Int get() = delegate.length
+
+    override fun get(index: Int): Char {
+        if ((counter++ and CHECK_MASK) == 0 && Thread.currentThread().isInterrupted) {
+            throw RegexInterruptedException()
         }
+        return delegate[index]
+    }
+
+    override fun subSequence(startIndex: Int, endIndex: Int): CharSequence =
+        InterruptibleCharSequence(delegate.subSequence(startIndex, endIndex))
+
+    override fun toString(): String = delegate.toString()
+
+    private companion object {
+        const val CHECK_MASK = 0x3FF
     }
 }
+
+private class RegexInterruptedException : RuntimeException() {
+    override fun fillInStackTrace(): Throwable = this
+}