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

三、总结

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

相关推荐
jyan_敬言9 分钟前
【C++】string类(二)相关接口介绍及其使用
android·开发语言·c++·青少年编程·visual studio
程序员老刘29 分钟前
Android 16开发者全解读
android·flutter·客户端
Java技术小馆1 小时前
GitDiagram如何让你的GitHub项目可视化
java·后端·面试
UGOTNOSHOT1 小时前
7.4项目一问题准备
面试
福柯柯1 小时前
Android ContentProvider的使用
android·contenprovider
不想迷路的小男孩1 小时前
Android Studio 中Palette跟Component Tree面板消失怎么恢复正常
android·ide·android studio
餐桌上的王子1 小时前
Android 构建可管理生命周期的应用(一)
android
菠萝加点糖2 小时前
Android Camera2 + OpenGL离屏渲染示例
android·opengl·camera
用户2018792831672 小时前
🌟 童话:四大Context徽章诞生记
android
yzpyzp2 小时前
Android studio在点击运行按钮时执行过程中输出的compileDebugKotlin 这个任务是由gradle执行的吗
android·gradle·android studio