Android中Timer与TimerTask引发内存泄漏的问题求助
解决TimerTask导致的Android内存泄漏问题
问题背景
在自定义View(LinearDotsLoader)中使用Timer()和scheduleAtFixedRate()实现圆点加载动画,已在视图不可见时调用Timer.cancel(),但LeakCanary检测到TimerTask存在内存泄漏,导致View无法被GC回收。
相关代码片段
定时器调度代码
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()) }
视图可见性变化处理
override fun onVisibilityChanged(changedView: View, visibility: Int) { super.onVisibilityChanged(changedView, visibility) if (visibility != VISIBLE) { timer?.cancel() } else if (shouldAnimate) { scheduleTimer() } }
完整自定义View代码
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() } } }
解决方案
原因分析
- 内部类强引用外部View:
DotsTimerTask是非静态内部类,默认持有LinearDotsLoader的强引用,即使调用Timer.cancel(),未执行的任务仍会保留该引用,导致View无法被GC回收。 - 定时器销毁不彻底:仅在
onVisibilityChanged中取消定时器,未处理View从Window移除的场景(如Activity销毁),存在遗漏。
修复步骤
- 将内部类改为静态内部类+WeakReference
静态内部类不持有外部类强引用,通过WeakReference弱引用持有View,确保View可被正常回收:
private class DotsTimerTask(private val loaderRef: WeakReference<LinearDotsLoader>) : TimerTask() { override fun run() { val loader = loaderRef.get() ?: return // View已回收则直接终止任务 loader.updateSelectedDot() } }
- 修改定时器初始化逻辑
private fun scheduleTimer() { timer = Timer() val dotsTimerTask = DotsTimerTask(WeakReference(this)) timer?.scheduleAtFixedRate(dotsTimerTask, 0, animDur.toLong()) }
- 完善View销毁时的定时器清理
添加onDetachedFromWindow方法,确保View从Window移除时彻底停止定时器:
override fun onDetachedFromWindow() { super.onDetachedFromWindow() timer?.cancel() timer?.purge() timer = null // 置空引用,辅助GC回收 }
- 优化UI线程切换逻辑
无需递归查找Activity,直接使用View的post方法切换到UI线程:
private fun updateSelectedDot() { // ... 原有逻辑不变 ... post { invalidate() } }
额外建议
- 优先使用
Handler+Runnable或Kotlin协程替代Timer,前者更适配Android UI场景,后者自带生命周期管理,可避免手动处理引用泄漏问题。
内容的提问来源于stack exchange,提问作者Rohit Singla
相关产品推荐
相关产品推荐

