AutoJs6 icon indicating copy to clipboard operation
AutoJs6 copied to clipboard

AutoJs6占用7GB内存

Open xjqm-tao opened this issue 8 months ago • 13 comments

手机:Redmi Note 11T Pro,安卓13 MIUI 14.0.7 已知AutoJs6 版本:6.6.2、6.6.1 Image

我的脚本中使用多个线程,高频使用images.findImage()函数,其它地方的image对象不使用了后都及时使用了recycle()回收。但随着脚本运行RAM占用越来越大,高达7GB,然后使其它软件崩溃。即使结束了脚本,AutoJs6仍然占7GB内存,“最近任务”和手机管家都不能清除AutoJs6,除非在应用详情页手动停止本软件运行才能释放内存。 怀疑是images.findImage()函数有问题。

这个情况经常发生。

xjqm-tao avatar Apr 25 '25 08:04 xjqm-tao

看下AI的分析:https://deepwiki.com/search/imagesfindimageautojs6ramrambu_475c2f12-0d86-4649-82f9-f7c5caa9657f

xjqm-tao avatar Apr 27 '25 10:04 xjqm-tao

没人??

xjqm-tao avatar May 06 '25 23:05 xjqm-tao

??

xjqm-tao avatar May 12 '25 02:05 xjqm-tao

不建议 Auto.js 使用大量线程并行执行操作, 你可以尝试在代码层面达到同等目标的同时, 尽量减低线程数量的使用.

AutoJs6 提供了 ImageWrapper#oneShot 方法, 可用于方法结束后自动回收图像资源. 例如:

let max = 10e3;
while (max--) {
    console.log(images.findImage(images.read('imgA.png').oneShot(), images.read('imgB.png').oneShot()));
}

上述示例代码进行了一万次图像查找 (仅作示例), 本地读取的图像 (imgA 与 imgB) 均进行了 oneShot() 标记, 这样在每次 findImage 方法结束之后, 都会自动回收图像资源.

注: images.captureScreen() 得到的图像通常不需要显示回收.

如仍有问题, 可提供实际使用的关键代码, 以便于进一步分析.

SuperMonster003 avatar Jun 09 '25 13:06 SuperMonster003

非常感谢大佬作者一直以来的更新 @SuperMonster003 在我看来 AutoJS6比AutoJSPro都好用。 尤其是在开发体验上,AutoJS6的插件更加的方便 相信作者是非常注重这个的项目的才会在体验上如此下功夫 但是目前Autojs最严重最致命的问题 就是findImage内存泄露的问题。 而且非常容易复现 即使我每次都回收了图片 但是他依然存在泄露问题 我仔细阅读了autojs findImage的源码 但是对于Android 我不太熟悉 我也在本地调试应用并使用LeakCanary尝试发现点什么 然后什么都没有发现 但是内存确实是在一次次的findImage调用后上涨。 经过验证 在 6.5版本并没有泄露问题 6.6.0版本开始就有内存泄露问题了。复现的代码如下。 如果可能的话 烦请大佬帮忙解答下大概是哪里出问题了。

let runtime = java.lang.Runtime.getRuntime();

log('初始的状态: ' + getMemoryData());

function getMemoryData() {
  let maxMemory = runtime.maxMemory(); // 能从操作系统那里挖到的最大的内存 单位字节Byte 536870912B === 512MB
  let totalMemory = runtime.totalMemory(); // 已经从操作系统那里挖过来的内存大小, 慢慢挖, 用多少挖多少
  let freeMemory = runtime.freeMemory(); // 挖过来而又没有用上的内存, 一般情况下都是很小

  let arr = [];
  arr.push('最大内存 = ' + (maxMemory / 1024 / 1024).toFixed(2) + 'MB');
  arr.push('已用内存 = ' + (totalMemory / 1024 / 1024).toFixed(2) + 'MB');
  arr.push('空闲内存 = ' + (freeMemory / 1024 / 1024).toFixed(2) + 'MB');
  return arr.join(', ');
}

let max = 10e3;
let index = 0;
let img1;
let img2;
while (index < max) {
  img1 = images.read('/sdcard/Pictures/test.png');
  img2 = images.read('/sdcard/Pictures/test1.png');
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  images.findImage(img1, img2);
  img1.recycle();
  img2.recycle();
  if (index % 10 === 0) {
    runtime.gc();
    log(getMemoryData());
  }
  index++;
  sleep(200);
}

DarrenHou1993 avatar Jun 28 '25 10:06 DarrenHou1993

经对比测试 Auto.js 4.1.1 Alpha2 与 AutoJs6 6.6.4, 确实存在 AutoJs6 运行上述代码后出现内存泄漏的问题 (Auto.js 4.x 正常). 我会仔细核查 AutoJs6 6.6.0 与 6.5.0 以及 Auto.js 4.x 之间的代码差异, 并在近期给出异常出现的原因及可能的解决方案.

Sent from my XQ-DQ72 using FastHub

SuperMonster003 avatar Jun 29 '25 09:06 SuperMonster003

@SuperMonster003 大佬不用看了 我这段代码的问题找到了。。。 原来是 log 的问题

// log(getMemoryData());
const memoryText = getMemoryData();
log(memoryText);

改成这样内存就不增长了... 按道理来说 getMemoryData 返回的是个字符串 不应该啊。..

DarrenHou1993 avatar Jun 29 '25 10:06 DarrenHou1993

@SuperMonster003 大佬忽略我上个回答。 Rhino 引擎可能和js引擎有点不一样。 在循环里面使用 const来定义数据会将数据缓存起来。。 所以希望看不到内存增长了。 其实内存还是在增长。

DarrenHou1993 avatar Jun 30 '25 14:06 DarrenHou1993

6.6.0 版本开始, 参考 Auto.js Pro 引入了 MonitorResource 接口, Cleaner 类等, 试图实现自动回收机制. 但直到 6.6.4 也没有完全实现, 反而因 ConcurrentHashMap 持有强引用对象导致 GC 无法正常回收 ImageWrapper 对象引起内存泄露.

去除与 MonitorResource 及 Cleaner 等相关的代码后, 可解决上述内存泄漏问题, 参考代码如下 (org.autojs.autojs.core.image.ImageWrapper.kt):

open class ImageWrapper : Shootable<ImageWrapper> {

    private var mScriptRuntime: ScriptRuntime

    private var mMat: Mat? = null
    private var mBgrMat: Mat? = null
    private var mBitmap: Bitmap? = null
    private var mMediaImage: Image? = null
    private var mPlane: Image.Plane? = null

    private var mWidth = 0
    private var mHeight = 0
    private var mIsRecycled = false
    private var mIsOneShot = false

    val width
        get() = mWidth.also { ensureNotRecycled() }

    val height
        get() = mHeight.also { ensureNotRecycled() }

    val size
        get() = Size(mWidth.toDouble(), mHeight.toDouble()).also { ensureNotRecycled() }

    val bitmap: Bitmap
        get() {
            ensureNotRecycled()
            if (mBitmap == null) {
                val mat = mMat
                if (mat != null) {
                    val bitmap = createBitmap(mat.width(), mat.height()).also { mBitmap = it }
                    Utils.matToBitmap(mat, bitmap)
                } else {
                    mMediaImage?.let { mBitmap = toBitmap(it) }
                }
            }
            return mBitmap ?: throw Exception("Bitmap of ImageWrapper should never be null")
        }

    val mat: Mat
        get() {
            ensureNotRecycled()
            val mat = mMat
            if (mat != null) {
                return mat
            }
            val bitmap = mBitmap
            if (bitmap != null) {
                val newMat = Mat().also { mMat = it }
                Utils.bitmapToMat(bitmap, newMat)
                return newMat
            }
            if (mMediaImage != null) {
                val plane = plane ?: throw AssertionError("Image plain is null")
                plane.buffer.position(0)
                return Mat(mHeight, mWidth, CvType.CV_8UC4, plane.buffer, plane.rowStride.toLong()).also { mMat = it }
            }
            throw AssertionError("Both bitmap and image are null")
        }

    val bgrMat
        get() = Mat().also {
            Imgproc.cvtColor(mat, it, Imgproc.COLOR_BGRA2BGR)
            mBgrMat = it
        }

    var plane: Image.Plane?
        private set(plane) {
            mPlane = plane
        }
        get() = mPlane ?: mMediaImage?.planes?.get(0)

    constructor(scriptRuntime: ScriptRuntime, width: Int, height: Int) : this(scriptRuntime, createBitmap(width, height))

    constructor(scriptRuntime: ScriptRuntime, bitmap: Bitmap) {
        mScriptRuntime = scriptRuntime
        mBitmap = bitmap.also { addToList(it) }
        mWidth = bitmap.width
        mHeight = bitmap.height
    }

    constructor(scriptRuntime: ScriptRuntime, mat: Mat) {
        mScriptRuntime = scriptRuntime
        mMat = mat.also { addToList(it) }
        mWidth = mat.cols()
        mHeight = mat.rows()
    }

    constructor(scriptRuntime: ScriptRuntime, mat: org.opencv.core.Mat) {
        mScriptRuntime = scriptRuntime
        mMat = when (mat.nativeObj != 0L) {
            true -> Mat(mat.nativeObj)
            else -> Mat(mat.rows(), mat.cols(), mat.type())
        }.also { addToList(it) }
        mWidth = mat.cols()
        mHeight = mat.rows()
    }

    constructor(scriptRuntime: ScriptRuntime, bitmap: Bitmap, mat: Mat?) {
        mScriptRuntime = scriptRuntime
        mMat = mat?.also { addToList(it) }
        mBitmap = bitmap.also { addToList(it) }
        mWidth = bitmap.width
        mHeight = bitmap.height
    }

    constructor(scriptRuntime: ScriptRuntime, mediaImage: Image) {
        mScriptRuntime = scriptRuntime
        mMediaImage = mediaImage.also { addToList(it) }
        mWidth = mediaImage.width
        mHeight = mediaImage.height
    }

    init {
        Images.initOpenCvIfNeeded()
    }

    private fun addToList(image: Any) {
        imageList.add(WeakReference(image))
    }

    fun saveTo(path: String): Boolean {
        ensureNotRecycled()
        val fullPath = mScriptRuntime.files.nonNullPath(path)
        if (mBitmap == null) {
            if (mMat != null) {
                return Imgcodecs.imwrite(fullPath, mMat)
            }
            bitmap /* Getter, for initializing `mBitmap`. */
        }
        return runCatching { saveWithBitmap(fullPath) }.isSuccess
    }

    private fun saveWithBitmap(fullPath: String?) {
        try {
            fullPath ?: throw Exception("Argument \"path\" cannot be null")
            mBitmap ?: throw Exception("Member \"bitmap\" cannot be null")
            mBitmap!!.compress(CompressFormat.PNG, 100, FileOutputStream(fullPath))
        } catch (e: FileNotFoundException) {
            throw UncheckedIOException(e)
        }
    }

    fun pixel(x: Int, y: Int): Int {
        ensureNotRecycled()
        if (x < 0 || y < 0 || x >= width || y >= height) {
            throw ArrayIndexOutOfBoundsException("Point ($x, $y) out of bounds of $this")
        }
        mBitmap?.let { oBitmap ->
            return oBitmap[x, y]
        }
        mMat?.let { oMat ->

            // @Caution by SuperMonster003 on Oct 30, 2024.
            //  ! Not that we should pass (y, x) instead of (x, y) here.
            //  ! Pay attention to the params of `org.opencv.core.Mat.get(row, col)` method,
            //  ! where "row" corresponds "y", and "col" corresponds "x".
            //  ! zh-CN:
            //  ! 这里需要注意不能传入 (x, y), 而需要传入 (y, x).
            //  ! 注意 `org.opencv.core.Mat.get(row, col)` 方法参数, "row" 对应 "y", "col" 对应 "x".
            val pixelList = oMat.get(/* row = */ y, /* col = */ x) ?: throw Exception("Channel list is null at ($x, $y) of $this")

            // @Hint by SuperMonster003 on Apr 9, 2025.
            //  ! Here we assume that pixelList contains ARGB channels.
            //  ! If mMat does not have these channels, an IndexOutOfBoundsException will be triggered:
            //  # java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
            //  ! Therefore, it is necessary to consider cases with different numbers of channels.
            //  !
            //  ! Refer to: http://issues.autojs6.com/350
            //  !
            //  ! zh-CN:
            //  ! 此处默认 pixelList 为 ARGB 通道, 当 mMat 不是上述通道时, 将触发索引越界异常:
            //  # java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
            //  ! 因此需要考虑不同通道数量的情况.
            //  !
            //  ! 参阅: http://issues.autojs6.com/350
            //  !
            //  # val (r, g, b, a) = pixelList.map { pixelValue -> pixelValue.toInt() }
            //  # return Color.argb(a, r, g, b)

            return when (pixelList.size) {
                1 -> {
                    // Single-channel image (grayscale/binary/...)
                    // zh-CN: 单通道图像 (灰度图/二值图/...)
                    val value = pixelList[0].toInt()
                    Color.argb(255, value, value, value)
                }
                3 -> {
                    // RGB
                    val (r, g, b) = pixelList.map { it.toInt() }
                    Color.argb(255, r, g, b)
                }
                in 4..Int.MAX_VALUE -> {
                    // RGBA
                    val (r, g, b, a) = pixelList.map { it.toInt() }
                    Color.argb(a, r, g, b)
                }
                else -> {
                    throw Exception("Unsupported pixel channel count (${pixelList.size}) at ($x, $y) of $this")
                }
            }
        }
        plane?.let { oPlane ->
            val buffer = oPlane.buffer.apply { position(0) }
            return abgrToArgb(buffer.getInt(oPlane.pixelStride * x + oPlane.rowStride * y))
        }
        throw Exception("At least one of bitmap, mat mad plane must be non-null")
    }

    override fun recycle() {
        synchronized(this) {
            mBitmap?.let {
                it.recycle()
                mBitmap = null
            }
            mMat?.let {
                OpenCVHelper.release(it)
                mMat = null
            }
            mBgrMat?.let {
                OpenCVHelper.release(it)
                mBgrMat = null
            }
            mMediaImage?.let {
                it.close()
                mMediaImage = null
                mPlane = null
            }
            mIsRecycled = true
        }
    }

    override fun setOneShot(b: Boolean) = also { mIsOneShot = b }

    override fun shoot() {
        if (mIsOneShot) recycle()
    }

    override fun isRecycled() = mIsRecycled

    @ScriptInterface
    fun ensureNotRecycled() {
        check(!isRecycled) { str(R.string.error_image_has_been_recycled) }
    }

    fun clone(): ImageWrapper {
        ensureNotRecycled()
        return when (val bitmap = mBitmap) {
            null -> ofMat(mScriptRuntime, mat.clone())
            else -> ofBitmap(mScriptRuntime, bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, true))
        }
    }

    /* 伴生对象代码省略. */
    /* companion object { ... ... } */
}

你可以尝试下载并试用项目程序包:

如问题复现, 可继续反馈.

SuperMonster003 avatar Jul 01 '25 04:07 SuperMonster003

@SuperMonster003 经过验证 内存上涨大福减少 10个小时大概上涨20M. 我有空在测试下Autojs6.5的版本10个小时上涨多少。 感谢大佬。 能分享下如何修改的吗 我尝试在Autojs6.6.4清理你所说的相关代码 跑不起来了。

DarrenHou1993 avatar Jul 05 '25 01:07 DarrenHou1993

尝试下载 6_6_4_-Patch-修复_images_部分相关方法可能引发内存泄露的问题(issue_#372).patch 到本地

IDE 菜单 (以 IntelliJ IDEA 2025.1.3 为例) Git | Patch | Apply patch... 选择 *.patch 文件所在路径 应用补丁到 AutoJs6 6.6.4 项目源码

SuperMonster003 avatar Jul 05 '25 11:07 SuperMonster003

你可以尝试下载并试用项目程序包:

如问题复现, 可继续反馈.

我试用了autojs6-v6.7.0-alpha2,内存占用还是会很大。我没头绪了。

xjqm-tao avatar Jul 14 '25 06:07 xjqm-tao

@xjqm-tao 若仍有高内存占用情况, 也可能并非 images 模块相关方法本身的问题, 参阅 #403.

SuperMonster003 avatar Jul 29 '25 09:07 SuperMonster003