一、发现问题
在日常体验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()
}
三、总结
以上我们简单的介绍了一下整个问题排查、优化的过程。本文借助一个内存泄漏问题,来简单剖析一下实现一个生产-消费的工具类都应该注意哪些事项。希望通过笔者的描述能够给读者代码一些启迪,一起加油。