由Handler引发的思考

Posted by Chejdj Blog on March 24, 2019

    先说一下,这个问题,我自己花了一晚上去思考这个问题,起源于大家都耳熟能详的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无法释放
leekCanary
但是代码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. 静态内部类+弱引用方法: 从跟源断开
    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){
                 //....
             }
         }
    }
    
  2. 在Activity销毁的时候,把在MessageQueue中的该Handler发送的Message清空(就像上面listener处理一样)
1
2
3
4
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(null);
}

第二种方法,有问题有的Activty可能有handler,有的没有,不好定义在BaseActivity中,第一种方法,如果很多Activity需要使用Handler,那每一个Activity是不是都要定义一个,当然我们还是需要对它进行封装一下,看了一下网上封装比较好的一个文件->WeakHandelr直通车,看到项目的随手点个赞啊!!