【Android Bug Fix】Thread使用不当导致的泄漏问题

一、发现问题

在日常体验App时,发现某个页面存在内存泄漏,基于此排查解决最终发现这个问题非典型,可以拿来简单聊一下,所以在这里简单记录一下。

二、排查问题

1、排查过程

发现泄漏后,通过AS工具 Analyze Memory Usage Heap Dump,查看泄漏的Activity/Fragment,很容易就找到了导致内漏的原因:

同事将某个Activity的对象(后文称Activity对象)作为Context,通过构造方法传入到某个对象(后文称对象a)中了。而有意思的是持有Activity对象的对象a并不是一个单例,并且同事在Activity onDestory()方法也调用对象a的destory方法做了相关的处理。

那为什么还会泄漏呢?

2、示例代码

要说清楚问题所在,需要贴一下A类的代码,去除业务逻辑简化的代码大致如下:

kotlin 复制代码
class A(val context: Context) : Thread() {

    private val waitingQueue: ArrayBlockingQueue<B>
    private var isStartWorking = false

    init {
        waitingQueue = ArrayBlockingQueue<B>(10)
        isStartWorking = true
        start()
    }

    fun destroy() {
        isStartWorking = false
        waitingQueue.clear()
    }

    override fun run() {
        super.run()
        while (isStartWorking) {
            try {
                val b = waitingQueue.take()
                if (b != null) {
                    parseQrCodeInThread(b!!)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
    
    fun handleB(b B) {
        ...
    }
    
    ...
}

可以看到A类本身是一个Thread,内部有两个变量值得注意:

一个是waitingQueue队列用于缓存业务任务,线程启动后在run方法中循环从队列中获取任务,队列不为空拿到任务后就去处理,当队列为空时则阻塞等待。

一个是isStartWorking,用于控制run方法是否退出。初始化为true,调用destory方法后为false。

整体实现一个可以依次排队,在工作线程中解决耗时任务的逻辑。

3、泄漏原因

A类构造方法中需要传入context对象,上层在使用时传入了Activity对象,而且在destory方法中并没有释放context对象,这是泄漏的直接原因。

但是对象a并不是一个单例对象,为何会持有activity对象不释放呢?

进一步review代码会发现这个类的实现存在很多问题,下面依次聊一下。

4、存在的问题

1)、Thread并不会退出

最初设计代码逻辑时,希望的是Thread A在有任务时执行任务,无任务时阻塞。当页面退出时将变量 isStartWorking 设置为false,线程退出死循环释放。

可实际运行下来并不是这样,进入Activity页面后很快任务执行完成,线程处于阻塞状态。当用户退出页面时,虽然通过调用destory方法将变量isStartWorking 设置为false,但由于当前线程已经处于阻塞状态了,后续队列也不会继续有任务添加进来,其会一直处于阻塞状态,并不会执行外层的if判断。这样整个线程就不会结束了。

当线程没有退出时,其对象也会被ThreadGroup线程组持有,最终导致该页面泄漏。

一种优化方法就是使用Application替换Activity,这样就可以解决泄漏问题。

2)、代码中并不需要context对象

使用Application替换Activity实际上这也不是最优的方法,因为review代码发现A类中实际上就不需要context对象,同事在开发这个类时无脑传入一个context对象以备后续使用。这种设计本身是存在很多问题的,不需要使用时就不要向上层要更多的数据。

3)、线程也泄露了

仅是解决了内存泄漏问题并不够,每次进入页面都会创先一个线程然后阻塞,相当于线程也泄漏了。要解决这个问题需要在A类destory方法中,调用interrupt方法中断线程。

4)、线程未设置名字

A类没有设置线程名字,会导致后期优化线程时很难排查,在init方法中应该优先设置线程的名字。

5、还可以如何优化

A类本质上是想实现一个可以依次排队,在工作线程中解决耗时任务的逻辑。

1)、方案一:HandlerThread

想要实现这个能力,首先可以考虑使用HandlerThread。HandlerThread 本身是Handler机制+Thread,通过Handler机制保证任务在队列中依次执行,退出线程时也可以直接调用quitSafely方法安全优雅的退出。

HandlerThread中quitSafely方法:

java 复制代码
public boolean quitSafely() {
    Looper looper = getLooper();
    if (looper != null) {
        looper.quitSafely();
        return true;
    }
    return false;
}

2)、方案二:使用协程

使用协程中的Channel,实现一个类似生产者消费者的模式。

kotlin 复制代码
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val channel = Channel<Unit>()

    // 任务处理器
    launch {
        for (task in channel) {
            println("Task started")
            delay(1000) // 模拟耗时任务
            println("Task completed")
        }
    }

    // 提交任务
    repeat(5) {
        channel.send(Unit)
    }

    // 关闭 Channel
    channel.close()
}

三、总结

以上我们简单的介绍了一下整个问题排查、优化的过程。本文借助一个内存泄漏问题,来简单剖析一下实现一个生产-消费的工具类都应该注意哪些事项。希望通过笔者的描述能够给读者代码一些启迪,一起加油。

相关推荐
二闹5 分钟前
大厂前端研发岗位设计的30道Webpack面试题及解析
前端·面试
福娃B14 分钟前
【CSS】面试必会—浮动布局:让元素“漂浮”的艺术
前端·css·面试
ZzMemory17 分钟前
详解JavaScript 解构赋值:让你的代码更优雅
前端·javascript·面试
PineappleCoder19 分钟前
CSS那些你不得不懂的“潜规则”(二)
前端·css·面试
用户20187928316723 分钟前
Binder 事务失败(FAILED BINDER TRANSACTION)
android
张元清27 分钟前
一个usePrevious引发的血案
javascript·react.js·面试
法欧特斯卡雷特29 分钟前
【译】Spring I&O 社区专家聊 Jimmer ORM
后端·spring·面试
胡gh36 分钟前
新朋友:Typescript,TypeScript 在 React 业务开发中的最佳实践
react.js·面试·typescript
AI大模型40 分钟前
超强大模型LLM面试八股文,54道题背完就超过70%的IT人!
程序员·llm·agent
柿蒂1 小时前
Android图片批量添加处理优化:从「30」秒缩短至「4.4」秒
android·android jetpack