4.1Handler基础使用
目录介绍
- 00.Handler必备知识点
- 01.Handler常见使用方式
- 02.在子线程中定义Handler
- 03.post方法使用的说明
- 04.避免子线程手动创建looper
- 05.Handler建议指定线程
- 06.不建议在子线程访问UI
- 07.子线程更新UI方式
- 7.1 子线程更新UI有哪些方式
- 7.2 runOnUiThread更新UI
- 7.3 handler.post更新UI
- 7.4 View.post()更新UI
- 08.解决Handler内存泄漏
00.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()处理消息。
image
01.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(1); } }.start(); } }); } }
02.在子线程中定义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(); } });
- 如何正确运行。在这里问一个问题,在子线程中可以吐司吗?
- 答案是可以的,只不过又条件。具体如下所示:
- 这样程序已经不会报错,那么这说明初始化Handler对象的时候我们是需要调用Looper.prepare()的,那么主线程中为什么可以直接初始化Handler呢?难道是主线程创建handler对象的时候,会自动调用Looper.prepare()方法的吗?博客
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(); } });
03.post方法使用的说明
3.1 Handler的post方法和view的post方法
- Handler的post方法实现很简单,如下所示
mHandler.post(new Runnable() { @Override public void run() { } }); public final boolean post(Runnable r){ return sendMessageDelayed(getPostMessage(r), 0); }
- view的post方法也很简单,如下所示
- 可以发现其调用的就是activity中默认保存的handler对象的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++; } }
04.避免子线程手动创建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();
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内存泄露主要2点
- 有延时消息,要在Activity销毁的时候移除Messages
- 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。博客
- 问题代码
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引起内存泄漏问题,需要我们在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 这种方式。每次使用前注意判空。