6.7.0 - Alpha18 - 修复 toast 方法因持有 Toast 实例强引用而导致可能得内存泄露问题

This commit is contained in:
SuperMonster003
2026-01-30 20:57:09 +08:00
parent 1dce33b180
commit 8205409c8c
8 changed files with 213 additions and 119 deletions

View File

@@ -1,7 +1,7 @@
{
"$data": {
"v6.7.0": {
"released_date": "2026/01/29",
"released_date": "2026/01/30",
"feature": [
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
"Paddle OCR (PP-OCRv5) 插件, 用于光学字符识别",
@@ -61,6 +61,7 @@
"console.build 方法的 (title/content)BackgroundColor 选项导致透明度或着色选项被覆盖的问题 _[`issue #458`](http://issues.autojs6.com/458)_",
"console/toast 等方法显示 Java Double 时可能丢失末尾零的问题",
"无法使用 console/toast 等方法显示 BigInt 数据类型的问题",
"toast 方法因持有 Toast 实例强引用而导致可能得内存泄露问题",
"部分全局对象可能丢失 JavaScript 原型属性及方法的问题",
"使用 XML 语法将 JavaScript 表达式作为属性值时, this 对象可能出现指向错误的问题",
"dialogs.build 方法 linkify 选项参数功能失效的问题",

View File

@@ -18,19 +18,18 @@ import org.autojs.autojs.core.console.GlobalConsole
import org.autojs.autojs.core.image.capture.ScreenCaptureRequester
import org.autojs.autojs.core.looper.Loopers
import org.autojs.autojs.core.pref.Pref
import org.autojs.autojs.rhino.extension.ScriptableExtensions.defineProp
import org.autojs.autojs.rhino.extension.ScriptableExtensions.prop
import org.autojs.autojs.lang.ThreadCompat
import org.autojs.autojs.pio.PFiles
import org.autojs.autojs.pio.UncheckedIOException
import org.autojs.autojs.rhino.AndroidClassLoader
import org.autojs.autojs.rhino.ProxyObject
import org.autojs.autojs.rhino.TopLevelScope
import org.autojs.autojs.rhino.extension.ScriptableExtensions.defineProp
import org.autojs.autojs.rhino.extension.ScriptableExtensions.prop
import org.autojs.autojs.runtime.api.AbstractShell
import org.autojs.autojs.runtime.api.AppUtils
import org.autojs.autojs.runtime.api.ProcessShell
import org.autojs.autojs.runtime.api.ScreenMetrics
import org.autojs.autojs.runtime.api.ScriptToast
import org.autojs.autojs.runtime.api.WrappedShizuku
import org.autojs.autojs.runtime.api.augment.app.App
import org.autojs.autojs.runtime.api.augment.autojs.Autojs
@@ -183,7 +182,7 @@ class ScriptRuntime private constructor(builder: Builder) {
private val mJob = SupervisorJob()
val coroutineScope = CoroutineScope(Dispatchers.Default + mJob)
val coroutineContext = coroutineScope.coroutineContext
val ownerId = "runtime@${System.identityHashCode(this)}"
private var mUiHandlerAppContext: Context
@@ -587,13 +586,13 @@ class ScriptRuntime private constructor(builder: Builder) {
Log.d(TAG, "on exit")
this.isExiting = true
ignoresException({
ignoresException {
if (console.configurator.isExitOnClose) {
console.hideDelayed()
}
})
}
ignoresException({ CoreWebSocket.onExit("Triggered by $TAG") })
ignoresException { CoreWebSocket.onExit("Triggered by $TAG") }
// @Hint by 抠脚本人 (https://github.com/little-alei) on Jul 10, 2023.
// ! 清空无障碍事件.
@@ -604,35 +603,42 @@ class ScriptRuntime private constructor(builder: Builder) {
// ! by the current script to avoid affecting other still-running scripts.
// ! zh-CN: 只清理当前脚本注册的无障碍事件回调,避免影响其他仍在运行的脚本.
// # ignoresException({ AccessibilityService.clearAccessibilityEventCallback() })
ignoresException({ automator.removeAllEventsForThisRuntime() })
ignoresException { automator.removeAllEventsForThisRuntime() }
ignoresException({ RootUtils.resetRuntimeOverriddenRootModeState() })
ignoresException { RootUtils.resetRuntimeOverriddenRootModeState() }
/* 回收全部记录的 ImageWrapper 实例. */
ignoresException({ CoreImageWrapper.recycleAll() })
// Recycle all recorded ImageWrapper instances.
// zh-CN: 回收全部记录的 ImageWrapper 实例.
ignoresException { CoreImageWrapper.recycleAll() }
/* 清除 interrupt 状态. */
ignoresException({ ThreadCompat.interrupted() })
// Clear interrupt status.
// zh-CN: 清除 interrupt 状态.
ignoresException { ThreadCompat.interrupted() }
/* 浮动窗口需要第一时间关闭. */
/* 以免出现恶意脚本全屏浮动窗口遮蔽屏幕并且在 exit 中写死循环的问题. */
ignoresException({ floaty.closeAll() })
// Floating windows need to be closed immediately.
// To prevent malicious scripts from creating fullscreen floating windows
// that obscure the screen and write infinite loops in exit.
// zh-CN:
// 浮动窗口需要第一时间关闭.
// 以免出现恶意脚本全屏浮动窗口遮蔽屏幕并且在 exit 中写死循环的问题.
ignoresException { floaty.closeAll() }
ignoresException({ events.emit("exit") }, "Exception on exit: %s")
ignoresException({ ScriptToast.clear(this) }, 500)
ignoresException("Exception on exit: %s") {
events.emit("exit")
}
ignoresException({ threads.shutDownAll() })
ignoresException({ events.recycle() })
ignoresException({ media.recycle() })
ignoresException({ loopers.recycle() })
ignoresException({ this.recycleShell() })
ignoresException({ images.releaseScreenCapturer() })
ignoresException({ images.stopScreenCapturerForegroundService() })
ignoresException({ ocrMLKit.release() })
ignoresException({ sensors.unregisterAll() })
ignoresException({ timers.recycle() })
ignoresException({ ui.recycle() })
ignoresException({ closeableManager.recycleAll() })
ignoresException { threads.shutDownAll() }
ignoresException { events.recycle() }
ignoresException { media.recycle() }
ignoresException { loopers.recycle() }
ignoresException { recycleShell() }
ignoresException { images.releaseScreenCapturer() }
ignoresException { images.stopScreenCapturerForegroundService() }
ignoresException { ocrMLKit.release() }
ignoresException { sensors.unregisterAll() }
ignoresException { timers.recycle() }
ignoresException { ui.recycle() }
ignoresException { closeableManager.recycleAll() }
}
fun setScreenMetrics(width: Int, height: Int) {
@@ -709,11 +715,11 @@ class ScriptRuntime private constructor(builder: Builder) {
}
}
private fun ignoresException(r: Runnable, @Suppress("SameParameterValue") delay: Int) {
uiHandler.postDelayed(r, delay.toLong())
private fun ignoresException(r: Runnable) {
ignoresException(null, r)
}
private fun ignoresException(r: Runnable, consoleMessage: String? = null) {
private fun ignoresException(consoleMessage: String?, r: Runnable) {
try {
r.run()
} catch (e: Throwable) {

View File

@@ -1,40 +1,155 @@
package org.autojs.autojs.runtime.api
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import org.autojs.autojs.runtime.ScriptRuntime
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap
import java.util.ArrayDeque
/**
* Toast global queue manager.
* All scripts share a single global queue because only one toast can be shown at a time.
*
* zh-CN:
*
* Toast 全局队列管理器.
* 所有脚本共享同一个全局队列, 因为同一时间通常只能显示一个 Toast.
*
* Created by SuperMonster003 on Aug 3, 2023.
* Modified by SuperMonster003 as of Oct 24, 2023.
* Transformed by SuperMonster003 on Oct 24, 2023.
* Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 30, 2026.
* Modified by SuperMonster003 as of Jan 30, 2026.
*/
object ScriptToast {
@JvmField
val pool = ConcurrentHashMap<Toast, WeakReference<ScriptRuntime>>()
private const val MAX_GLOBAL_QUEUE_SIZE = 17
private val mainHandler = Handler(Looper.getMainLooper())
private val queue = ArrayDeque<ToastTask>(MAX_GLOBAL_QUEUE_SIZE)
private var isShowing = false
private var currentTask: ToastTask? = null
private var currentToast: Toast? = null
private var currentFinishRunnable: Runnable? = null
private data class ToastTask(
val message: String,
val duration: Int,
val ownerId: String,
)
@JvmStatic
fun add(t: Toast, scriptRuntime: ScriptRuntime) {
pool[t] = WeakReference(scriptRuntime)
fun enqueueToast(context: Context, scriptRuntime: ScriptRuntime, message: String, isLong: Boolean = false) {
val duration = if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
val ownerId = scriptRuntime.ownerId
// Must be called on the main thread.
// zh-CN: 必须在主线程调用.
if (Looper.myLooper() != Looper.getMainLooper()) {
mainHandler.post { enqueueToast(context, scriptRuntime, message, isLong) }
return
}
if (queue.size >= MAX_GLOBAL_QUEUE_SIZE) {
// Discard newest task when the queue is full.
// zh-CN: 当队列已满时丢弃最新任务.
return
}
queue.addLast(ToastTask(message, duration, ownerId))
tryShowNextLocked(context.applicationContext)
}
@JvmStatic
fun dismissAll(scriptRuntime: ScriptRuntime) {
pool.entries.forEach { entry ->
if (entry.value.get() == scriptRuntime) {
pool.remove(entry.key)
entry.key.cancel()
val ownerId = scriptRuntime.ownerId
mainHandler.post {
queue.removeAll { it.ownerId == ownerId }
if (currentTask?.ownerId == ownerId) {
currentFinishRunnable?.let { mainHandler.removeCallbacks(it) }
currentFinishRunnable = null
currentToast?.cancel()
currentToast = null
currentTask = null
isShowing = false
tryShowNextLocked(scriptRuntime.uiHandler.applicationContext)
}
}
}
@JvmStatic
fun clear(scriptRuntime: ScriptRuntime) {
pool.entries
.filter { it.value.get() == scriptRuntime }
.forEach { pool.remove(it.key) }
val ownerId = scriptRuntime.ownerId
mainHandler.post {
queue.removeAll { it.ownerId == ownerId }
}
}
@JvmStatic
fun destroy() {
mainHandler.post {
queue.clear()
currentFinishRunnable?.let { mainHandler.removeCallbacks(it) }
currentFinishRunnable = null
currentToast?.cancel()
currentToast = null
currentTask = null
isShowing = false
}
}
private fun tryShowNextLocked(appCtx: Context) {
if (isShowing) return
val task = queue.removeFirstOrNull() ?: return
isShowing = true
currentTask = task
val toast = Toast.makeText(appCtx, task.message, task.duration)
currentToast = toast
val finish = Runnable {
currentFinishRunnable = null
currentToast = null
currentTask = null
isShowing = false
tryShowNextLocked(appCtx)
}
currentFinishRunnable = finish
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
toast.addCallback(object : Toast.Callback() {
override fun onToastHidden() {
super.onToastHidden()
finish.run()
}
})
} else {
val delay = if (task.duration == Toast.LENGTH_LONG) 3_500L else 2_000L
mainHandler.postDelayed(finish, delay)
}
toast.show()
}
private fun <E> ArrayDeque<E>.removeFirstOrNull(): E? =
if (isEmpty()) null else removeFirst()
private inline fun <E> ArrayDeque<E>.removeAll(predicate: (E) -> Boolean) {
if (isEmpty()) return
val it = iterator()
while (it.hasNext()) {
if (predicate(it.next())) it.remove()
}
}
}

View File

@@ -1,13 +1,13 @@
package org.autojs.autojs.runtime.api.augment.toast
import org.autojs.autojs.AutoJs
import org.autojs.autojs.annotation.RhinoFunctionBody
import org.autojs.autojs.annotation.RhinoRuntimeFunctionInterface
import org.autojs.autojs.rhino.ArgumentGuards
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.ScriptToast
import org.autojs.autojs.runtime.api.augment.Augmentable
import org.autojs.autojs.runtime.api.augment.Invokable
import org.autojs.autojs.util.RhinoUtils.undefined
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
import org.autojs.autojs.util.RhinoUtils.UNDEFINED
import org.mozilla.javascript.Undefined
class Toast(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), Invokable {
@@ -35,26 +35,22 @@ class Toast(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntim
@JvmStatic
@RhinoRuntimeFunctionInterface
fun call(scriptRuntime: ScriptRuntime, args: Array<out Any?>): Undefined = ensureArgumentsLengthInRange(args, 0..3) {
undefined {
when (it.size) {
0 -> ToastParser("").show(scriptRuntime)
1 -> ToastParser(args[0]).show(scriptRuntime)
2 -> ToastParser(args[0], args[1]).show(scriptRuntime)
3 -> ToastParser(args[0], args[1], args[2]).show(scriptRuntime)
}
val toastParser = when (it.size) {
0 -> ToastParser(globalContext, "")
1 -> ToastParser(globalContext, args[0])
2 -> ToastParser(globalContext, args[0], args[1])
3 -> ToastParser(globalContext, args[0], args[1], args[2])
else -> throw WrappedIllegalArgumentException("Invalid arguments length ${args.size} for global.toast")
}
toastParser.show(scriptRuntime)
return@ensureArgumentsLengthInRange UNDEFINED
}
@JvmStatic
@RhinoRuntimeFunctionInterface
fun dismissAll(scriptRuntime: ScriptRuntime, args: Array<out Any?>): Undefined = ensureArgumentsIsEmpty(args) {
dismissAllRhino(scriptRuntime)
}
@JvmStatic
@RhinoFunctionBody
fun dismissAllRhino(scriptRuntime: ScriptRuntime) = undefined {
AutoJs.instance.uiHandler.dismissAllToasts(scriptRuntime)
ScriptToast.dismissAll(scriptRuntime)
return@ensureArgumentsIsEmpty UNDEFINED
}
}

View File

@@ -1,14 +1,18 @@
package org.autojs.autojs.runtime.api.augment.toast
import org.autojs.autojs.AutoJs
import org.autojs.autojs.rhino.extension.AnyExtensions.jsBrief
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.util.RhinoUtils.coerceString
import org.mozilla.javascript.Context
import org.autojs.autojs.runtime.api.ScriptToast
import org.mozilla.javascript.Scriptable.NOT_FOUND
import org.mozilla.javascript.Undefined
import android.content.Context as AndroidContext
import org.mozilla.javascript.Context as RhinoContext
class ToastParser(private val msg: Any?, long: Any? = null, forcible: Any? = null) {
/**
* Created by SuperMonster003 on Dec 2, 2024.
* Modified by SuperMonster003 as of Jan 30, 2026.
*/
class ToastParser(private val context: AndroidContext, private val msg: Any?, isLong: Any? = null, isForcible: Any? = null) {
private val badge = object {
val long = Regex("^l(ong)?$", RegexOption.IGNORE_CASE)
@@ -20,42 +24,42 @@ class ToastParser(private val msg: Any?, long: Any? = null, forcible: Any? = nul
var isForcible = false
init {
parseIsLong(long)
parseIsForcible(forcible)
parseIsLong(isLong)
parseIsForcible(isForcible)
}
fun show(scriptRuntime: ScriptRuntime) {
if (isForcible) Toast.dismissAllRhino(scriptRuntime)
if (this@ToastParser.isForcible) ScriptToast.dismissAll(scriptRuntime)
val niceMsg = when {
Undefined.isUndefined(msg) || msg == NOT_FOUND -> "undefined"
msg == null -> "null"
else -> Context.toString(msg)
else -> RhinoContext.toString(msg)
}
AutoJs.instance.uiHandler.toast(scriptRuntime, niceMsg, isLong)
ScriptToast.enqueueToast(context, scriptRuntime, niceMsg, this@ToastParser.isLong)
}
private fun parseIsLong(o: Any?) = when (o) {
is Boolean -> o
is Number -> Context.toBoolean(o)
is Number -> RhinoContext.toBoolean(o)
is String -> when {
badge.long.matches(o) -> true
badge.short.matches(o) -> false
badge.forcible.matches(o) -> isLong.also { this.isForcible = true }
badge.forcible.matches(o) -> this@ToastParser.isLong.also { this.isForcible = true }
else -> throw IllegalArgumentException("Argument \"isLong\" ${o.jsBrief()} for Toast.Parser is invalid")
}
else -> isLong
}.also { isLong = it }
else -> this@ToastParser.isLong
}.also { this@ToastParser.isLong = it }
private fun parseIsForcible(o: Any?) = when (o) {
is Boolean -> o
is Number -> Context.toBoolean(o)
is Number -> RhinoContext.toBoolean(o)
is String -> when {
badge.forcible.matches(o) -> true
badge.long.matches(o) -> isForcible.also { this.isLong = true }
badge.short.matches(o) -> isForcible.also { this.isLong = false }
badge.long.matches(o) -> this@ToastParser.isForcible.also { this.isLong = true }
badge.short.matches(o) -> this@ToastParser.isForcible.also { this.isLong = false }
else -> throw IllegalArgumentException("Argument \"isForcible\" ${o.jsBrief()} for Toast.Parser is invalid")
}
else -> isForcible
}.also { isForcible = it }
else -> this@ToastParser.isForcible
}.also { this@ToastParser.isForcible = it }
}

View File

@@ -3,33 +3,10 @@ package org.autojs.autojs.tool
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import android.widget.Toast.LENGTH_SHORT
import org.autojs.autojs.AutoJs
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.runtime.api.ScriptToast
import org.autojs.autojs.util.RhinoUtils.isMainThread
/**
* Created by Stardust on May 2, 2017.
* Transformed by SuperMonster003 on Oct 25, 2023.
* Modified by SuperMonster003 as of Jan 30, 2026.
*/
class UiHandler(val applicationContext: Context) : Handler(Looper.getMainLooper()) {
fun toast(scriptRuntime: ScriptRuntime, message: String, isLong: Boolean = false) {
val showRunnable = Runnable {
val toastLength = if (isLong) LENGTH_LONG else LENGTH_SHORT
Toast.makeText(AutoJs.instance.application, message, toastLength)
.also { ScriptToast.add(it, scriptRuntime) }
.show()
}
when {
isMainThread() -> showRunnable.run()
else -> this@UiHandler.post(showRunnable)
}
}
fun dismissAllToasts(scriptRuntime: ScriptRuntime) = ScriptToast.dismissAll(scriptRuntime)
}
class UiHandler(val applicationContext: Context) : Handler(Looper.getMainLooper())

View File

@@ -6,8 +6,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
@@ -347,12 +345,9 @@ class MainActivity : BaseActivity(), DelegateHost, HostActivity {
}
private fun setUpStatusBarIconLight() {
Handler(Looper.getMainLooper()).post {
if (sIsActionBarDrawerOpened) {
setUpStatusBarIconLightByNightMode()
} else {
setUpStatusBarIconLightByThemeColor()
}
when (sIsActionBarDrawerOpened) {
true -> setUpStatusBarIconLightByNightMode()
else -> setUpStatusBarIconLightByThemeColor()
}
}

View File

@@ -1,5 +1,5 @@
#Wed Jan 28 21:05:38 CST 2026
BUILD_TIME=1769605538543
#Fri Jan 30 20:56:33 CST 2026
BUILD_TIME=1769777793096
COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3666
VERSION_BUILD=3673
VERSION_NAME=6.7.0 Alpha18
VSCODE_EXT_REQUIRED_VERSION=1.0.13