最近看了handler相关代码,将所学handler内存泄漏相关总结一下。
1.handler内存泄漏根本原因:
sThreadLocal是GC Roots,可达Activity。故Activity不会被回收。
2.下面分析整个流程
2.1我们常使用Handler的方式是在主线程中构造一个Handler对象。如下
Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
java匿名内部类会持有外部类引用,故handler持有activity,即handler->activity。
这里说下java匿名内部类是怎么持有外部类引用的(知道这个知识的,可以直接看2.2)
我们简单定义一个外部类和匿名内部类
public class Out {
private String tag = "Out";
InnerClass innerClass = new InnerClass() {
@Override
void printTag() {
System.out.printf(tag);
}
};
}
public class InnerClass {
void printTag() {
}
}
在命令行中使用javac Out.java InnerClass.java,将Out.java和InnerClass.java编译成class文件,会编译出3个class文件,
- Out.class - 外部类Out的字节码文件
- InnerClass.class - InnerClass类的字节码文件
- Out$1.class - 匿名内部类的字节码文件
Out$1.class就是匿名内部类的字节码文件,在AS中打开这个class文件,如下:
class Out$1 extends InnerClass {
Out$1(final Out var1) {
this.this$0 = var1;
}
void printTag() {
System.out.printf(this.this$0.tag);
}
}
可以看到内部类的构造方法中传入了外部类的对象, 并赋值给了内部类的一个成员变量this$0,所以匿名内部类会持有外部类的引用
2.2下面看Handler类的代码
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
Handler类的enqueueMessage()方法第一句:msg.target = this;message持有handler,即message->handler。
之后是MessageQueue持有Message,这就不作分析了。现有持有链:messageQueue->message->handler->activity
2.3然后看下messageQueue是被谁持有了,在Looper的构造方法中
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
从这里看出looper持有messageQueue。即looper->messageQueue。
2.4接着看下Looper.prepare()方法做了什么
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
这里将looper对象放进sThreadLocal。sThreadLocal持有looper。
2.5我们继续看sThreadLocal是什么。
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
sThreadLocal是一个静态变量,即GC Roots。
至此,我们完整的可达链为sThreadLocal->looper->messageQueue->message->handler->activity。
总结:由于sThreadLocal是GC Root,activity是可达的,所以activity不会被回收,就会造成内存泄漏。
3.修改handler内存泄漏
- 修改Handler为static类,并使用弱引用,这样才能在静态内部类中调用外部类方法。
此种方式是打断handler->activity。因为java静态匿名内部类不会持有外部类引用。
下面代码是Kotlin中使用Handler的方式(静态内部类 + 弱引用)
class MainActivity : AppCompatActivity() {
companion object {
private const val MY_MESSAGE = 1
}
private val handler by lazy { StaticHandler(this) }
lateinit var binding: ActivityMainBinding
/**
* 静态内部类 + 弱引用
*/
class StaticHandler(obj: MainActivity) : Handler(Looper.getMainLooper()) {
private val ref = WeakReference(obj)
override fun handleMessage(msg: Message) {
ref.get()?.apply {
when(msg.what) {
MY_MESSAGE -> Log.d("mainActivity", "handleMessage: ${getData()}")
}
}
}
}
fun getData() : Int {
return 2
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val message = Message.obtain()
message.what = myMessage
handler.sendMessage(message)
}
}
注意:在Kotlin中,嵌套类默认是static的,如果想让内部类持有外部类,使用 inner 关键字
- 在activity.onDestory()方法中,将messageQueue中的消息清除掉。
此种方式是打断messageQueue->message。
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
4.GC Roots是什么
以下引用自刘望舒大佬的著作《android进阶解密》:
选定一些对象作为GC Roots,并组成根对象集合,然后以这些GC Roots的对象作为起始点,向下搜索,如果目标对象到GC Roots是连接着的,我们则称该目标对象是可达的,如果目标对象不可达则说明目标对象是可以被回收的对象。
…
在java中,可以作为GC Roots的对象主要有以下几种:
1.java栈中引用的对象
2.本地方法栈中JNI引用的对象
3.方法区中运行时常量池引用的对象
4.方法区中静态属性引用的对象
5.运行中的线程
6.由引导类加载器加载的对象
7.GC控制的对象