先说一下,这个问题,我自己花了一晚上去思考这个问题,起源于大家都耳熟能详的Handler在Activity容易导致内存泄漏,我相信大家都知道,但是这里面的细致原因,却之前自己没有深究,所以写一下这篇博客,记录一下自己的思考。
Handler导致内存泄漏
我们都知道Handler因为持有外部Activity引用导致了,Activity的无法释放,但是为什么Handler持有Activity的引用??
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}; //代码 1
// 代码2 Handler handler = new Handler();
handler.sendEmptyMessageDelayed(1, 20000);
initentMain2();
}
private void initentMain2() {
Intent intent = new Intent(this, Main2Activity.class);
startActivity(intent);
MainActivity.this.finish();
}
}
可以在手机上安装LeakCanary测试一下上面 1和2代码,会发现代码1会导致内存泄漏,持有一条messageQuee ->message->handler->activity引用链导致Activity无法释放
但是代码2 不会导致内存泄漏,聪明的你应该看出来了,因为内部类默认持有外部类的引用,代码1采用了匿名内部类的构造方法,所以引用了外部Activity的引用。至于为什么内部类默认持有外部类的引用,翻看.class文件可以看到构建内部类的时候,会传入外部类的this指针给内部类,具体可以参考Java内部类详解
思考一:类的成员变量持有一个对象的引用,该对象持有它的引用吗?
看下面代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MainActivity.java
public class MainActivity extends AppCompatActivity {
CommonHandler handler = new CommonHandler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler.sendEmptyMessageDelayed(1, 20000);
initentMain1();
}
private void initentMain1() {
Intent intent = new Intent(this, Main2Activity.class);
startActivity(intent);
MainActivity.this.finish();
}
}
//CommonHandler.java
public class CommonHandler extends Handler{
}
这个如何判断,我之前一直在纠结是否持有呢?,运行一下项目,发现LeakCanary并没有报处内存泄漏的Bugs,所以可以从客观上断定handler并没有持有MainActivity的引用。那怎么样才算是持有MainActivity的引用呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// MainActivity.java
public class MainActivity extends AppCompatActivity {
CommonHandler handler = new CommonHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler.sendEmptyMessageDelayed(1, 20000);
initentMain1();
}
private void initentMain1() {
Intent intent = new Intent(this, Main2Activity.class);
startActivity(intent);
MainActivity.this.finish();
}
}
//CommonHandler.java 注释1
public class CommonHandler extends Handler{
private Activity activity;
public CommonHander(Activity activity){
this.activity=activity;
}
}
//CommonHandler.java 注释2
public class CommonHandler extends Handler{
public CommonHandler(Activity activity){
//没有用成员变量存储下来
}
}
让CommonHandler持有MainActivity的引用,我们可以直接传入一个this引用给他,但是这里面我有一个疑问,传入了指针但是并没有用类的变量储存这个引用,那么还算是引用了它吗? 我分别测试了注释1,注释2,发现只有注释1导致了内存泄漏,注释2并没有导致内存泄漏,为什么呢?解释这个问题,我们需要重新回顾一下Java运行时数据区程序计数器,Java栈,本地方法栈,方法区,堆以及可作为GCRoot的对象
Java运行时数据区
- 程序计数器: 保存程序当前执行的指令地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令的时候,从程序计数器得到当前需要执行指令的地址。
- Java栈,也称虚拟机栈:Java栈存放一个个栈帧,每一个栈帧对应一个被调用的方法,栈帧里面存储:局部变量表,操作数栈,指向当前方法所属类的运行时常量池的引用,方法返回地址
(1)局部变量:包含方法中声明的非静态变量和函数形参,基本类型就存储值,引用类型变量就存储指向对象的引用
(2)操作数栈:表达式的求值就是栈的一个应用,程序中所有的计算都是通过他
(3)运行时常量池:方法执行的时候可能会用到类中的常量,所以必须有一个引用指向他
(4)f方法返回地址:就是方法执行完之后,需要返回到它调用的那个位置 - 本地方法栈:和Java栈原理相似,不过是针对本地方法服务
- 堆:存储对象本身和数组的
- 方法区: 与堆一样被线程共享,存储每个类的信息(包括类的名称,方法信息,字段信息),静态变量,常量以及编译器编译后的代码。这里面也有一个非常重要的部分运行时常量池,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
GCRoot对象
- 虚拟机栈引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用对象
现在可以解释了,因为构造函数也是一个函数,在执行的时候,也是以栈帧的形式,函数形参存储在栈帧上,当方法执行完之后,就弹栈,栈帧里面的变量就会销毁掉,所以注释2实际上CommonHandler没有持有Activity的引用,但是注释1里面的成员变量Activity 存储在堆上面,只要CommonHandler对象存活,会一直持有着Activity的引用。
思考二:为什么setOnClickListener(new View.OnClickListener)没有导致Activity内存泄漏?
经常Andorid代码,下面的代码应该是非常常见的
1
2
3
4
5
6
7
8
//MainActivity.java
Button btn=findViewById(R.id.button);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
....
}
});
这里的OnClickListener采用的是匿名内部类,也会默认持有外部类的引用,这里存在着window->view->listerner->activity这样一条链,那为什么没有出现过Activity的泄漏呢?这里我们需要查看源码看一下View在销毁的时候做了什么?这里不明白的同学可以查看一下View的生命周期直通车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* This is a framework-internal mirror of onDetachedFromWindow() that's called
* after onDetachedFromWindow().
*
* If you override this you *MUST* call super.onDetachedFromWindowInternal()!
* The super method should be called at the end of the overridden method to ensure
* subclasses are destroyed first
*
* @hide
*/
@CallSuper
protected void onDetachedFromWindowInternal() {
mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
mPrivateFlags3 &= ~PFLAG3_TEMPORARY_DETACH;
removeUnsetPressCallback(); //注释1
removeLongPressCallback(); //注释2
removePerformClickCallback();//注释3
cancel(mSendViewScrolledAccessibilityEvent);
stopNestedScroll();
// Anything that started animating right before detach should already
// be in its final state when re-attached.
jumpDrawablesToCurrentState();
destroyDrawingCache();
cleanupDraw();
mCurrentAnimation = null;
if ((mViewFlags & TOOLTIP) == TOOLTIP) {
hideTooltip();
}
}
看注释,我们知道这个方法就是在和Window解除绑定的时候才调用的,也就是在View销毁的时候调用,可以看到注释1,注释2中,去除了两个对于Unset和Press的计时器,我们重点关注注释3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void removePerformClickCallback() {
if (mPerformClick != null) {
removeCallbacks(mPerformClick);
}
}
public boolean removeCallbacks(Runnable action) {
if (action != null) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mHandler.removeCallbacks(action);
attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
Choreographer.CALLBACK_ANIMATION, action, null);
}
getRunQueue().removeCallbacks(action);
}
return true;
}
OncliCk事件也是通过Handler发送的,mPerformClick是阻塞中的在消息队列的Runnable对象,把阻塞的事件清除掉
由此我们应该能够得到真正listener导致Activity无法释放的引用链,应该是MessageQueue->mPerformClick->listener->ActivityMessageQueue的生命周期为全局的(此处的Handler也是主线程的Handler),所以导致这个链不能释放,但是我们View在销毁的时候,把它从队列中销毁了,所以链mPerformClick->listener->Activity,mPerformClick没有其他GCRoot引用它,这条链直接销毁,断开了listerner和actviity的引用,不会导致Activity的内存泄漏。
结论:listener原本会导致Activity的内存泄漏,只是View帮我们完美的规避好了,所以我们还是要敬慎的写每一个内部类,千万不要用一个生命周期更长的对象引用它,导致外部类释放不了
完美的Handler的实现方法
- 静态内部类+弱引用方法: 从跟源断开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
//MainActivity.java static class CommonHandler extends Handler{ private WeakReference<Activity> weakReference; public CommonHandler(Activity activity) { weakReference=new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); Activity activity=weakReference.get(); if(activity!=null){ //.... } } }
- 在Activity销毁的时候,把在MessageQueue中的该Handler发送的Message清空(就像上面listener处理一样)
1
2
3
4
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
第二种方法,有问题有的Activty可能有handler,有的没有,不好定义在BaseActivity中,第一种方法,如果很多Activity需要使用Handler,那每一个Activity是不是都要定义一个,当然我们还是需要对它进行封装一下,看了一下网上封装比较好的一个文件->WeakHandelr直通车,看到项目的随手点个赞啊!!