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

三、总结

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

相关推荐
java熟手5 小时前
面试-JVM:JVM的组成及作用
jvm·面试
Yang-Never6 小时前
OpenGL ES -> GLSurfaceView绘制点、线、三角形、正方形、圆(索引法绘制)
android·java·开发语言·kotlin·android studio
扫地僧0097 小时前
Java 面试题及答案整理,最新面试题
java·jvm·算法·面试
Cheese%%Fate7 小时前
【C++】面试常问八股
c++·面试
huangkaihao8 小时前
无限滚动优化指南:从原理到实践
前端·面试·设计
Nicole Potter8 小时前
装箱和拆箱是什么?(C#)
开发语言·游戏·面试·c#
诸神黄昏EX8 小时前
Android 常用命令和工具解析之存储相关
android
迷路国王9 小时前
kotlin 知识点四 高阶函数详解 什么是内联函数
android·开发语言·kotlin
迷路国王9 小时前
kotlin 知识点五 泛型和委托
android·开发语言·kotlin