【问题标题】:Memory leak while using TimerTask in Timer of Android在 Android 的 Timer 中使用 TimerTask 时内存泄漏
【发布时间】:2022-11-09 02:41:33
【问题描述】:

我正在使用Timer() 对象在Android 中的活动中显示点加载器,并在scheduleAtFixedRate()Timer API 中传递TimerTask() 的对象。

这是我的代码 sn-p:-

private fun scheduleTimer() {
        timer = Timer()
        timer?.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                if (isSingleDir) {
                    selectedDotPos++
                    if (selectedDotPos > noOfDots) {
                        selectedDotPos = 1
                    }
                } else {
                    if (isFwdDir) {
                        selectedDotPos++
                        if (selectedDotPos == noOfDots) {
                            isFwdDir = !isFwdDir
                        }
                    } else {
                        selectedDotPos--
                        if (selectedDotPos == 1) {
                            isFwdDir = !isFwdDir
                        }
                    }
                }

                (scanForActivity(context))?.runOnUiThread {
                    invalidate()
                }
            }
        }, 0, animDur.toLong())
    }

当视图的可见性发生变化时,我确保通过调用Timer::cancel() 来取消计时器。这是代码: -

override fun onVisibilityChanged(changedView: View, visibility: Int) {
        super.onVisibilityChanged(changedView, visibility)

        if (visibility != VISIBLE) {
            timer?.cancel()
        } else if (shouldAnimate) {
            scheduleTimer()
        }
    }

但是TimerTask 正在泄漏一些内存。我正在使用 Leak Canary 来分析内存泄漏。这是报告:-

寻求帮助来解决这个问题。

我班级的完整代码:-

class LinearDotsLoader : DotsLoaderBaseView
{
    private var timer: Timer? = null

    var isSingleDir = true

    private var diffRadius: Int = 0
    private var isFwdDir = true

    constructor(context: Context) : super(context) {
        initCordinates()
        initPaints()
    }

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        initAttributes(attrs)
        initCordinates()
        initPaints()
        initShadowPaints()
    }

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        initAttributes(attrs)
        initCordinates()
        initPaints()
        initShadowPaints()
    }

    override fun initAttributes(attrs: AttributeSet) {

        super.initAttributes(attrs)

        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LinearDotsLoader, 0, 0)

        this.noOfDots = typedArray.getInt(R.styleable.LinearDotsLoader_loader_noOfDots, 3)

        this.selRadius = typedArray.getDimensionPixelSize(R.styleable.LinearDotsLoader_loader_selectedRadius, radius + 10)

        this.dotsDistance = typedArray.getDimensionPixelSize(R.styleable.LinearDotsLoader_loader_dotsDist, 15)

        this.isSingleDir = typedArray.getBoolean(R.styleable.LinearDotsLoader_loader_isSingleDir, false)
        this.expandOnSelect = typedArray.getBoolean(R.styleable.LinearDotsLoader_loader_expandOnSelect, false)

        typedArray.recycle()
    }

    override fun initCordinates() {
        diffRadius = this.selRadius - radius

        dotsXCorArr = FloatArray(this.noOfDots)

        //init X cordinates for all dots
        for (i in 0 until noOfDots) {
            dotsXCorArr[i] = (i * dotsDistance + (i * 2 + 1) * radius).toFloat()
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val calWidth: Int
        val calHeight: Int

        if (expandOnSelect) {
            calWidth = (2 * this.noOfDots * radius + (this.noOfDots - 1) * dotsDistance + 2 * diffRadius)
            calHeight = 2 * this.selRadius
        } else {
            calHeight = 2 * radius
            calWidth = (2 * this.noOfDots * radius + (this.noOfDots - 1) * dotsDistance)
        }
        setMeasuredDimension(calWidth, calHeight)
    }

    override fun onVisibilityChanged(changedView: View, visibility: Int) {
        super.onVisibilityChanged(changedView, visibility)

        if (visibility != VISIBLE) {
            timer?.cancel()
            timer?.purge()
        } else if (shouldAnimate) {
            scheduleTimer()
        }
    }

    private fun scheduleTimer() {
        timer = Timer()
        val dotsTimerTask = DotsTimerTask()
        timer?.scheduleAtFixedRate(dotsTimerTask, 0, animDur.toLong())
    }


    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawCircle(canvas)
    }

    private fun drawCircle(canvas: Canvas) {
        for (i in 0 until noOfDots) {

            var xCor = dotsXCorArr[i]
            if (expandOnSelect) {
                if (i + 1 == selectedDotPos) {
                    xCor += diffRadius.toFloat()
                } else if (i + 1 > selectedDotPos) {
                    xCor += (2 * diffRadius).toFloat()
                }
            }

            var firstShadowPos: Int
            var secondShadowPos: Int

            if ((isFwdDir && selectedDotPos > 1) || selectedDotPos == noOfDots) {
                firstShadowPos = selectedDotPos - 1
                secondShadowPos = firstShadowPos - 1
            } else {
                firstShadowPos = selectedDotPos + 1
                secondShadowPos = firstShadowPos + 1
            }

            if (i + 1 == selectedDotPos) {
                selectedCirclePaint?.let {
                    canvas.drawCircle(
                        xCor,
                        (if (expandOnSelect) this.selRadius else radius).toFloat(),
                        (if (expandOnSelect) this.selRadius else radius).toFloat(),
                        it
                    )
                }
            } else if (showRunningShadow && i + 1 == firstShadowPos) {
                canvas.drawCircle(
                    xCor,
                    (if (expandOnSelect) this.selRadius else radius).toFloat(),
                    radius.toFloat(),
                    firstShadowPaint)
            } else if (showRunningShadow && i + 1 == secondShadowPos) {
                canvas.drawCircle(
                    xCor,
                    (if (expandOnSelect) this.selRadius else radius).toFloat(),
                    radius.toFloat(),
                    secondShadowPaint)
            } else {
                defaultCirclePaint?.let {
                    canvas.drawCircle(
                        xCor,
                        (if (expandOnSelect) this.selRadius else radius).toFloat(),
                        radius.toFloat(),
                        it
                    )
                }
            }

        }
    }

    var dotsDistance: Int = 15
        set(value) {
            field = value
            initCordinates()
        }

    var noOfDots: Int = 3
        set(noOfDots) {
            field = noOfDots
            initCordinates()
        }

    var selRadius: Int = 38
        set(selRadius) {
            field = selRadius
            initCordinates()
        }

    var expandOnSelect: Boolean = false
        set(expandOnSelect) {
            field = expandOnSelect
            initCordinates()
        }

    private fun scanForActivity(context: Context?): Activity? {
        return when (context) {
            null -> null
            is Activity -> context
            is ContextWrapper -> scanForActivity(context.baseContext)
            else -> null
        }
    }

    private fun updateSelectedDot()
    {
        if (isSingleDir) {
            selectedDotPos++
            if (selectedDotPos > noOfDots) {
                selectedDotPos = 1
            }
        } else {
            if (isFwdDir) {
                selectedDotPos++
                if (selectedDotPos == noOfDots) {
                    isFwdDir = !isFwdDir
                }
            } else {
                selectedDotPos--
                if (selectedDotPos == 1) {
                    isFwdDir = !isFwdDir
                }
            }
        }

        (scanForActivity(context))?.runOnUiThread {
            invalidate()
        }
    }

    private inner class DotsTimerTask: TimerTask(){
        override fun run() {
            updateSelectedDot()
        }
    }

}

【问题讨论】:

  • onVisibilityChanged 是否在 onDestroy() 之前被调用?您还需要取消onDestroy 内的计时器。您还需要在 cancel 之后使用 timer.purge()
  • @ADM 是的 onVisibilityChangedonDestroy() 之前被调用。我已经验证了这一点。调用timer.purge() 是否确保不会有任何内存泄漏?因为我已经尝试过这样做。而且这种内存泄漏仍然存在。
  • 不能肯定地说。你到底在哪里有这个方法scheduleTimer 你能添加那个类的完整代码吗?看起来它的 Runnable 导致泄漏,因为你在其中使用了上下文.. 删除 context 使用并使用主线程处理程序在 UI 线程上发布。
  • @ADM我已经添加了我班级的完整代码。你能看看吗?

标签: android memory memory-leaks timer timertask


【解决方案1】:

答案是修复取消和实例化/调度计时器之间的竞争。

在每个 Timer 实例化和启动之前添加取消就足​​够了。

private fun scheduleTimer() {
    timer?.cancel() // fix
    timer = Timer()
    val dotsTimerTask = DotsTimerTask()
    timer?.scheduleAtFixedRate(dotsTimerTask, 0, animDur.toLong())
}

我在此代码的回购中打开了问题: https://github.com/agrawalsuneet/DotLoadersPack-Android/issues/44

【讨论】:

    最近更新 更多