Handler+Thread的一种封装尝试RxTask(上篇)

883 阅读6分钟

项目地址:github.com/AcgnCodeMon…

  说起handler,做安卓开发的同学应该都很熟悉。handler在我们日常开发中,主要用于子线中刷新ui,因为通常情况下,安卓系统是不允许我们在子线程中直接操作ui的,这点,大家在才开始学习安卓的时候,想必也一定听老师讲过,甚至有时候一不注意,可能也会在子线程中刷新ui,以至于程序崩溃。至于为什么不能在子线程刷新ui以及handler的通信原理,这里不是本文的重点,有兴趣的同学,可以自己查阅相关资料。

  我们先来看看传统的handler的用法,首先是这种:

 private Handler mHandler = new Handler() {
        @Override
        public void handleMessage (Message msg) {
            super.handleMessage(msg);
            Log.e("Handler", "Message");
            tv.setText("handler改变了控件文本");
            iv.setImageBitmap(mBitmap);
        }
    };
    new Thread(new Runnable() {
                    @Override
                    public void run () {
                        try {
                            Thread.sleep(TimeConfig.SLEEP_TIME);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        mHandler.sendEmptyMessage(0);
                    }
                }).start();

  这种写法,才学习安卓的同学们一定经常用到,简单粗暴。就是直接采用内部类对象的方式实例化一个handler,然后在子线程中通过这个handler发送对应消息进行ui操作。当然,稍有经验的同学都知道这种写法会造成内存泄漏,因为java中内部类(包括匿名内部类)对象会隐式的持有外部对象的引用,这样,如果耗时操作未完成之前我们退出了当前activity,就会造成activity因为被handler引用而无法回收。

  当然,稍有经验的同学们一定看到过别人说过一定要使用静态内部类写法,例如:

private MyHandler mHandler2;
   @Override
protected void onCreate (@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        tv = findViewById(R.id.tv);
        iv = findViewById(R.id.iv);
        mHandler2 = new MyHandler(this);
        new Thread(new Runnable() {
                    @Override
                    public void run () {
                        try {
                            Thread.sleep(TimeConfig.SLEEP_TIME);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        mHandler2.sendEmptyMessage(0);
                    }
                }).start();
    }
 private static class MyHandler extends Handler {
        private WeakReference<HandlerActivity> mReference;

        public MyHandler (HandlerActivity activity) {
            mReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage (Message msg) {
            super.handleMessage(msg);
            Log.e("Handler2", "Message");
            if (mReference.get() == null) {
                return;
            }
            mReference.get().tv.setText("handler2改变了控件文本");
            mReference.get().iv.setImageBitmap(mReference.get().mBitmap);

        }
    }

  那么这两种写法到底效果如何呢?让我们来运行一下,并用AS自带的内存分析,来看下结果。假设activity中有个耗时30秒的异步任务,任务完成后通过handler发送消息进行ui刷新,为了让内存变化更明显,我们在activity中声明并实例化一个加载了大图的bitmap,我们反复开启退出这个界面三次。首先看第一种写法的内存图:

  可以看到,随着我们反复开启关闭这个activity,应用的内存占用呈阶梯式上涨,我们完成操作后开始不停的用AS的GC进行内存回收,发现在一段时间内内存变化几乎为0,在过了一段时间后GC有了效果,内存占用同样开始呈阶梯式下降,最后回归未开启界面的水平。

  中间内存无法回收很好理解,因为activity发生了泄漏,bitmap无法被虚拟机回收(bitmap没有做recycle操作),但是为什么后面内存又能成功回收了呢?按照之前的想法,内存应该是一直无法回收的吧?其实不然,通过上面的代码我们可以看出,我们的子线程做了一个耗时操作(30秒),在这30秒内,我们确实是隐式持有这activity,造成其无法回收,但是,任务执行完毕后,我们用handler发送完消息,执行完毕后,我们就已经不再持有activity了,这时泄漏的activity也就可以顺利回收了,通过下面的时间轴可以证明我们的观点,第一次可以回收的点,基本上就是第一次打开activity并开启任务后过了30秒,后面同理,所以才会出现内存阶梯式下降。

  也就是说,传统写法,确实会造成在子线程执行期间activity无法被回收,那么下面让我来看看使用静态内部类+弱引用的方法,会不会有效呢?

  惊呆了有木有?童话里都是骗人的,说好的用静态内部类不会内存泄漏呢?上图和一图如出一辙,并没有什么太大的区别,难道是网上其他大佬在胡说?

  并不是,只是我们的代码有问题而已,的确,我们的handler确实使用了静态内部类+弱引用,但是我们忽略了一个问题,上面说过,匿名内部类也会隐式持有外部对象的。而这次我们的问题就出在thread上,是的,我们采用了匿名内部类的方式声明了一个子线程。而就是这个线程造成了,activity一直被子线程持有无法回收,直到子线程执行完毕。

  意思是说,我们用子线程也得像handler那样用静态内部类+弱引用?感觉整个人都不好了,有木有,静态内部类太麻烦,弱引用用起来更是麻烦,那么有没有什么解决方法呢?

当然是有的了。

  既然子线程和handler都容易造成activity泄漏,无法被回收,我们为什么不自己封装一下,在适当的时候切断子线程,handler和activity的关系呢?

  经过不断尝试,首先使用一个自定义类Emitter来持有handler,同时声明一个自定义类TaskCallable(实现自Callable接口)来实现子线程功能,然后创建一个自定义类Task来协调和调度Emitter及TaskCallable,以此来完成子线程和handler的交互。同时,引入线程池机制,改善到处new thread的low比做法,创建自定义类RxLifeList用于绑定activity的生命周期,这样就可以在activity的onDestroy方法被调用时及时的切断对activity的引用。

  说了那么多,想法很美好,实际效果如果呢?先上图再说:

  这里的逻辑和上面两个例子是一样的,不同之处在于使用了自己封装的类进行的操作,可以看到,退出界面后GC几乎就能立即顺利的回收掉activitiy了(不要问我为什么还是呈阶梯式下降,我也没想通,QAQ),代码如下:

public class TestRunnable extends TaskCallable {

    @Override
    public boolean run (Emitter emitter) throws Exception {
        Thread.sleep(TimeConfig.SLEEP_TIME);
        Log.e("TestRunnable","执行完毕!");
        return true;
    }
}

private RxLifeList mBindLife;


    @Override
    protected void onCreate (Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBindLife = new RxLifeList();
        iv = findViewById(R.id.iv);
    }
    @Override
    protected void onDestroy () {
      if (mBindLife != null) {
            mBindLife.onDestroy();
        }
        super.onDestroy();
    }
    //子线程任务部分
            RxExecutor.getInstance()
                        .executeTask(new TestRunnable(),
                                new Task(true) {
                                    @Override
                                    public void bindLife (RxLife rxLife) {
                                        super.bindLife(rxLife);
                                        mBindLife.add(rxLife);
                                    }

                                    @Override
                                    public void onError (Exception e) {
                                        super.onError(e);
                                        toast(e.getMessage() + "\n解绑handler,不再回调!");
                                    }

                                    @Override
                                    public void onFinished () {
                                        super.onFinished();
                                        tv.setText("handler改变了控件文本");
                                        iv.setImageBitmap(mBitmap);
                                        toast("绑定生命周期的任务执行完毕!");
                                    }
                                });

  不要吐槽代码风格,和命名。没错,我就是不要脸的抄袭rxJava的写法,-_-

这篇就到这里,下篇文章将重点讲解RxTask的使用方法,及优势。

  目前,RxTask这个项目还处于测试阶段,不过,本人会在近期开源到Jcenter,欢迎不怕踩坑的同学踊跃尝试。