项目地址: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 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,欢迎不怕踩坑的同学踊跃尝试。