Memory leak while using TimerTask in Timer of Android

413 Views Asked by At

I am showing a dots loader in my activity in Android using a Timer() object and passing an object of TimerTask() in the scheduleAtFixedRate() API of Timer.

This is my code snippet:-

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

I am making sure that to cancel the timer by calling Timer::cancel(), when the visibility of the view changes. This is the code:-

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

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

But the TimerTask is leaking some memory. I am using Leak Canary to analyze the memory leak. This is the report:-

enter image description here

Looking for help to resolve this.

Complete code of my class:-

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

}
1

There are 1 best solutions below

1
Roman Karanchuk On

The answer is to fix racing between cancelling and instantiate/schedule Timer.

To add cancel before each Timer instantiation and starting should be enough.

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

I opened issue on repo of this code: https://github.com/agrawalsuneet/DotLoadersPack-Android/issues/44