4.1Handler基础使用
目录介绍
- 01.Handler必备知识点
- 02.Handler常见使用方式
- 03.子线程中定义Handler
- 04.post方法使用的说明
- 05.Handler建议指定线程
- 06.不建议子线程访问UI
- 07.子线程更新UI方式
- 08.解决Handler内存泄漏
01.Handler必备知识点
消息机制作用:
跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。
消息机制四要素:
- Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。
- MessageQueue(消息队列):用来存放Handler发送过来的消息,通过 Handler 发送的消息并非是立即执行的,需要存入一个消息队列中来依次执行,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
- Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
- Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。Looper 不断从 MessageQueue 中获取消息并将之传递给消息处理者(即是消息发送者 Handler 本身)进行处理。
具体流程
- 发送消息:Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息;
- 取出消息:通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next();
- 处理消息:调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息。
02.Handler常见使用方式
handler机制大家都比较熟悉呢。在子线程中发送消息,主线程接受到消息并且处理逻辑。如下所示
一般handler的使用方式都是在主线程中定义Handler,然后在子线程中调用mHandler.sendXx()方法,这里有一个疑问可以在子线程中定义Handler吗?
public class MainActivity extends AppCompatActivity {
private TextView tv ;
/**
* 在主线程中定义Handler,并实现对应的handleMessage方法
*/
public static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 101) {
Log.i("MainActivity", "接收到handler消息...");
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
// 在子线程中发送异步消息
mHandler.sendEmptyMessage(101);
}
}.start();
}
});
}
}
03.子线程中定义Handler
直接在子线程中创建handler,看看会出现什么情况?博客
运行后可以得出在子线程中定义Handler对象出错,难道Handler对象的定义或者是初始化只能在主线程中?其实不是这样的,错误信息中提示的已经很明显了,在初始化Handler对象之前需要调用Looper.prepare()方法。
为何会报错?
Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。
Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
Log.i(TAG, "在子线程中定义Handler,接收并处理消息");
}
}
};
}
}.start();
}
});
如何正确运行。在这里问一个问题,在子线程中可以吐司吗?
答案是可以的,只不过又条件。具体如下所示:
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
Looper.prepare();
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
Log.i(TAG, "在子线程中定义Handler,接收并处理消息");
}
}
};
//获取Looper对象
mLooper = Looper.myLooper();
Looper.loop();
//在适当的时候退出Looper的消息循环,防止内存泄漏
mLooper.quit();
}
}.start();
}
});
这样程序已经不会报错,那么这说明初始化Handler对象的时候我们是需要调用Looper.prepare()的,那么主线程中为什么可以直接初始化Handler呢?
难道是主线程创建handler对象的时候,会自动调用Looper.prepare()方法的吗?这个可以看博客
一个线程中,Handler可以是多个,Looper必须只有一个。一个线程如何有多个Looper就会报错,如果你是设计者,你可以想一下,可以有多个消息发送者,但只能有一个消息轮询器处理消息!
避免子线程手动创建looper。下面这种使用方式,是非常危险的一种做法
在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。(【 Looper.myLooper().quit(); 】)
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();
04.post方法使用的说明
Handler的post方法实现很简单,如下所示
mHandler.post(new Runnable() {
@Override
public void run() {
}
});
public final boolean post(Runnable r){
return sendMessageDelayed(getPostMessage(r), 0);
}
view的post方法也很简单,如下所示
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
ViewRootImpl.getRunQueue().post(action);
return true;
}
public void post(Runnable action) {
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
可以发现其调用的就是activity中默认保存的handler对象的post方法
05.Handler建议指定线程
先来看一下使用案例,下面这两个有何区别?
Handler handler1 = new Handler();
Handler handler2 = new Handler(Looper.getMainLooper());
06.不建议子线程访问UI
这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:
- ①首先加上锁机制会让UI访问的逻辑变得复杂
- ②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
所以最简单且高效的方法就是采用单线程模型来处理UI操作。为什么说子线程不能更新UI?
- 子线程是不能直接更新UI的。Android实现View更新有两组方法,分别是invalidate和postInvalidate。
- 前者在UI线程中使用,后者在非UI线程即子线程中使用。换句话说,在子线程调用 invalidate 方法会导致线程不安全。
- 熟悉View工作原理的人都知道,invalidate 方法会通知 view 立即重绘,刷新界面。
- 作一个假设,现在用 invalidate 在子线程中刷新界面,同时UI线程也在用 invalidate 刷新界面,这样会不会导致界面的刷新不能同步?这就是invalidate不能在子线程中使用的原因。博客
07.子线程更新UI方式
7.1 子线程更新UI方式
主要有这些方法
- 主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI
- 用Activity对象的runOnUiThread方法
- 创建Handler,传入getMainLooper,使用handler.post更新UI
- View.post(Runnable r)
相似处:跟进去看源码,发现其实它们的实现原理都还是一样,最终都是通过Handler发送消息来实现的。
7.2 runOnUiThread更新UI
如何使用代码如下所示
new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
tv_0.setText("滚犊子++++");
}
});
}
}).start();
看看源码,如下所示
在runOnUiThread程序首先会判断当前线程是否是UI线程,如果是就直接运行,如果不是则post,这时其实质还是使用的Handler机制来处理线程与UI通讯。博客
@Override
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
7.3 handler.post更新UI
Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的。
在子线程中,是否也可以创建一个Handler,并获取MainLooper,从而在子线程中更新UI呢?首先我们看到,在Looper类中有静态对象sMainLooper,并且这个sMainLooper就是在ActivityThread中创建的MainLooper。
//在子线程中通过这个sMainLooper来进行更新UI操作
new Thread(new Runnable() {
@Override
public void run() {
Log.e("yc", "yc 4"+Thread.currentThread().getName());
//注意这里创建handler时,需要传入looper
Handler handler = new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
//Do Ui method
tv_0.setText("滚犊子————————————---");
Log.e("yc", "yc 5"+Thread.currentThread().getName());
}
});
}
}).start();
7.4 View.post()更新UI
代码如下所示
tv_0.post(new Runnable() {
@Override
public void run() {
tv_0.setText("滚犊子");
}
});
源码原理如下所示
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
View.post(Runnable r)使用注意事项
看源码的注释可知:如果view已经attached,则调用ViewRootImpl中的ViewRootHandler,放入主线程Lopper等待执行。如果detach,则将其暂存在RunQueue当中,等待其它线程取出执行。
View.post(Runnable r)很多时候在子线程调用,用于进行子线程无法完成的操作,或者在该方法中通过getMeasuredWidth()获取view的宽高。需要注意的是,在子线程调用该函数,可能不会被执行,原因是该view不是attached状态。博客
08.解决Handler内存泄漏
Handler内存泄漏问题代码
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler();
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.text); //模拟内存泄露
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mTextView.setText("yangchong");
}
}, 2000);
}
}
造成内存泄漏原因分析
上述代码通过内部类的方式创建mHandler对象,此时mHandler会隐式地持有一个外部类对象引用这里就是MainActivity,当执行postDelayed方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
解决Handler内存泄露主要2点
- 有延时消息,要在Activity销毁的时候移除Messages
- 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。博客
解决方案:第一种解决办法
要想避免Handler引起内存泄漏问题,需要我们在Activity关闭退出的时候的移除消息队列中所有消息和所有的Runnable。
上述代码只需在onDestroy()函数中调用mHandler.removeCallbacksAndMessages(null);就行了。
@Override
protected void onDestroy() {
super.onDestroy();
if(handler!=null){
handler.removeCallbacksAndMessages(null);
handler = null;
}
}
解决方案:第二种解决方案
使用弱引用解决handler内存泄漏问题,关于代码案例,可以参考我的开源项目:https://github.com/yangchong211/YCAudioPlayer中的utils-share包下的ShareDialog代码
//自定义handler
public static class HandlerHolder extends Handler {
WeakReference<OnReceiveMessageListener> mListenerWeakReference;
/**
* @param listener 收到消息回调接口
*/
HandlerHolder(OnReceiveMessageListener listener) {
mListenerWeakReference = new WeakReference<>(listener);
}
@Override
public void handleMessage(Message msg) {
if (mListenerWeakReference!=null && mListenerWeakReference.get()!=null){
mListenerWeakReference.get().handlerMessage(msg);
}
}
}
//创建handler对象
private HandlerHolder handler = new HandlerHolder(new OnReceiveMessageListener() {
@Override
public void handlerMessage(Message msg) {
switch (msg.what){
case 1:
TextView textView1 = (TextView) msg.obj;
showBottomInAnimation(textView1);
break;
case 2:
TextView textView2 = (TextView) msg.obj;
showBottomOutAnimation(textView2);
break;
}
}
});
//发送消息
Message message = new Message();
message.what = 1;
message.obj = textView;
handler.sendMessageDelayed(message,time);
即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。