Handler、Message、MessageQueue、Looper协作简析
为什么使用Handler
在Java里面线程间通信的方式有好些方式,在Android里我们常用的方式之一是通过Handler,做过点Android开发的都知道,在Android里对界面上的一些操作都需要放在主线程或者说UI线程中,其实不在主线程中也能操作,当然有特殊要求,具体请自行百度。总之,通过Handler我们能比较方便的在子线程和主线程间通信。
相关类
android.os.Handler
发送消息和处理消息都需要通过它。它会与一个Looper相关联。
android.os.Looper
通常我们叫轮询器,负责消息分发的。一个Looper只能和一个线程关联。
android.os.MessageQueue
消息队列,放消息的,每个Looper和线程最多只会有一个消息队列。
android.os.Message
它可以存放一些数据并传递给相应消费线程。
Handler的简单示例
public class MainActivity extends AppCompatActivity { private TextView mTvTip; private Button mBtnDownLoad; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { // what为0表示下载完成 case 0: mTvTip.setText("苍老师的电影下载好啦,还不快去鉴赏。"); break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTvTip = (TextView) findViewById(R.id.tv_tip); mBtnDownLoad = (Button) findViewById(R.id.btn_download); mBtnDownLoad.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { // 睡眠5秒钟模拟下载过程 SystemClock.sleep(1000 * 5); Message msg = mHandler.obtainMessage(); // 下载完成,将msg的what值设定为0 msg.what = 0; msg.sendToTarget(); } }).start(); } }); }}
代码很简单,界面就一个TextView
和一个Button
,给按钮设定了点击监听,点击的时候起了个线程去下载老师的艺术片儿,下载完后在界面上用文字通知咱们去鉴赏。
Handler的模式
从上面我们的示例代码可以看出,子线程将一个msg传递到主线程去处理,就相当于一个生产者消费者模式,子线程生产,主线程消费。如下图所示:
1、插入,生产者线程(对于我们上面的示例就是子线程)通过消费者线程(对于我们上面的示例就是UI线程)上的Handler将msg插入到消息队列里。
2、检索,运行在消费者线程的Looper会按顺序检索MessageQueue里的消息。
3、分发,消费者线程上的Handler负责处理msg,一个线程可以有多个Handler来处理msg,Looper会将相应的msg分发给对应的Handler来处理。
子线程使用Handler
之前我们的例子是在UI线程里实例化Handler的,所以UI线程可以接收到消息,那如何发送消息到子线程呢?我们试试在子线程里实例化Handler:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new DownloadThread().start(); } private class DownloadThread extends Thread { public Handler mHandler; @Override public void run() { mHandler = new Handler(); } }}
按之前的猜想,直接在子线程里实例化Handler,结果报错如下:
错误显示在实例化Handler之前需要先调用Looper.prepare()。我们来看一下Looper.prepare()的部分源码:
public static void prepare() { prepare(true);}private static void prepare(boolean quitAllowed) { // 检查线程局部变量是否已经保存了一个Looper if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } // 创建一个Looper对象并放到当前线程局部变量中 sThreadLocal.set(new Looper(quitAllowed));}
通过源码我们能发现prepare()方法就是将一个Looper对象放到当前线程的局部变量中,从它抛出的异常信息我们也能验证了之前说的一个线程只能有一个Looper对象。这和我们在实例化Handler对象需要先调用prepare()有什么关系呢?不难猜出是实例化Handler的时候需要用到当前线程的Looper对象,我们暂且相信是这样的,后面验证。
我们修改一下代码再试试:
public class MainActivity extends AppCompatActivity { private Button mBtnDownload; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBtnDownload = (Button) findViewById(R.id.btn_download); final DownloadThread downloadThread = new DownloadThread(); downloadThread.start(); mBtnDownload.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Message msg = downloadThread.mHandler.obtainMessage(); msg.what = 0; msg.sendToTarget(); } }); } private class DownloadThread extends Thread { public Handler mHandler; @Override public void run() { // 先实例化一个Looper放到当前线程的局部变量 Looper.prepare(); mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: Log.e("fiend", "接收到UI线程发过来的信息了"); break; } } }; } }}
代码很简单,点击按钮的时候向子线程发送消息,如果子线程收到消息会在Logcat里打印出一条log,但是点击按钮后并没有看到打印的log,这,这就很尴尬了。。。
通过我们写的代码,能够知道已经向子线程发送消息了,为毛子线程没反应呢,通过上面的生产者消费者模式的图我们可以知道,消息的检索是由Looper来做的,是不是Looper哪里出了问题,导致它没有检索到我们发送过来的消息?我们的代码只是调用了Looper.prepare()方法,而这个方法只是创建了一个Looper对象放到当前线程的局部变量中,是不是Looper还有个开关来控制它开始检索消息?我们来看看Looper的源码先,找了一阵后我们在Looper源码里发现了这么个方法:
/** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. */ public static void loop() { ...// 省略部分代码 for (;;) { ...// 省略部分代码 msg.target.dispatchMessage(msg); ...// 省略部分代码 } }
从loop()方法的注释我们就能知道这个方法是开启消息轮询的,主要是有个for的死循环一直调用target来分发消息,从Message的源码可以知道target就是Handler对象。我们再次修改一下代码:
private class DownloadThread extends Thread { public Handler mHandler; @Override public void run() { Looper.prepare(); mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: Log.e("fiend", "接收到UI线程发过来的信息了"); break; } } }; // 增加调用这个方法来开始消息轮询 Looper.loop(); } }
在实例化Handler后我们调用了Looper.loop()方法来开始消息的轮询,结果输出如下:
终于正常收到UI线程发过来的消息了。
停止Looper
上面我讲了子线程如何接收UI线程发送过来的消息。但是这里有个问题。如何结束这个子线程?
就像我们上面的下载线程,当苍老师的艺术片下载完了后,其实我们就已经不需要这个子线程了,但是从之前Looper的源码里我们知道它是个死循环在轮询消息,这样就导致我们的子线程无法正常结束了,这,这就有点浪费资源了。。。
我们继续查看Looper的源码,看一下是否有相关代码是关于如何退出死循环的。发现了这两个方法:
public void quit() { mQueue.quit(false);}public void quitSafely() { mQueue.quit(true);}
从方法的名字我们能知道的是一个是退出,一个是安全退出,通过它们的注释我们也能知道,它们就是退出Looper的方法,至于quit()方法是一开始就有的方法,但可能不安全,quitSafely()方法则是API 18(Android 4.3)后才有的方法,它能安全退出Looper.
这样我们就能在子线程做完所有工作后调用Looper.myLooper().quit()
方法,这样子线程就能正常结束了。
Handler对象实例化过程
前面我们留有一个疑问,为什么实例化Handler的时候需要先实例化一个Looper才行,我们来一探究竟。
Handler部分源码如下:
public Handler() { this(null, false);}public Handler(Callback callback, boolean async) { ...// 省略部分代码 // 获取当前线程的Looper mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; ...// 省略部分代码}
从上面的构造方法中我们看到之前在没调用Looper.prepare()
就实例化Handler
所抛出的异常信息,这也解释了为什么要先实例化Looper
后再实例化Handler
。
到这里我们应该会有个疑问,刚开始我们从子线程发消息到主线程的例子中,我们实例化Handler
的时候并没有调用Looper.prepare()
啊,为毛没有报错?这,这就有点尴尬了…
UI线程的Looper
继续查看Looper的源码,发现了这么一个方法:
public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); }}
从名字大概就能猜到,这是创建主Looper的方法,而且抛出的异常信息也能证明这一点。我们来看看是在哪里调用的这个方法:
通过查找调用的地方发现,在程序入口main()
方法里调用这个方法,所以这就解释了为什么我们在UI线程实例化Handler
的时候没调用Looper.prepare()
方法也没有报错,因为早就已经在入口处创建了UI线程的Looper
了,而且还是调用的Looper.prepareMainLooper()
方法,当然在这个方法里我们也看到它,它还是调用的prepare(false)
创建的。
注意,我们在子线程创建Looper
的时候是直接调用的prepare()
方法,并没有带参数,我们来看一下它的代码:
public static void prepare() { prepare(true);}
原来方法里也是带参数调用的,但是UI线程带的参数是false
,而别的线程带的参数是true
,从形参的命名我们就能知道是说能否退出Looper
的意思,之前我们说了,可能通过调用Looper.myLooper().quit()
方法退出Looper
,所以这里我们能够知道,UI线程的Looper
是不允许退出的。
MessageQueue
说了这么久,好像还没看到MessageQueue
在哪里实例化的,我们来看看,在Looper
实例化的时候代码是这样的:
// 方法一public static void prepare() { prepare(true);}// 方法二private static void prepare(boolean quitAllowed) { // 检查线程局部变量是否已经保存了一个Looper if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } // 创建一个Looper对象并放到当前线程局部变量中 sThreadLocal.set(new Looper(quitAllowed));}// 方法三private Looper(boolean quitAllowed) { // 实例化MesssageQueue mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread();}
方法一
和方法二
在之前已经解释过了,我们看方法二
最后一行代码,实例化了一个Looper对象放到当前线程的局部变量中,方法三
是Looper的构造方法,在构造方法里我们看到了MessageQueue
的实例化代码。之前我们说了一个线程只能有一个Looper
,而MessageQueue
是在Looper
的构造方法中实例化的,所在一个线程同样只能有一个MessageQueue
。
Message
最后我们来看看Message
,我们想要传递的内容就是放在Message
里,然后通过MessageQueue
最终传递到消费者线程去处理。Message
里可以放数据(data)或者任务(task),每个Message
只能放一种内容,不能同时存在数据和任务。
数据消息(Data message):
——数据消息可以携带这些信息。通过handleMessage()
处理。
任务消息(Task message):
——任务消息就是传递一个java.lang.Runnable
对象到消费者线程执行。
实例化Message
有几种方法:
- 直接调用构造方法
Message m = new Message();
- 调用工厂方法
Message m = Message.obtain();Message m = Message.obtain(Handler h);Message m = Message.obtain(Handler h, int what);Message m = Message.obtain(Handler h, int what, Object o);Message m = Message.obtain(Handler h, int what, int arg1, int arg2);Message m = Message.obtain(Handler h, int what, int arg1, int arg2,Object o);Message m = Message.obtain(Handler h, Runnable task);Message m = Message.obtain(Message originalMsg);
调用工厂方法好点,因为它会从Message池里获取,不需要new一个新的,池里没有才会new,所以性能稍微好点吧。
消息都是通过Handler
插入到消息队列的,数据消息是通过前缀为send
的方法插入的,任务消息是通过前缀为post
的方法插入的。
- 插入数据对象到消息队列
boolean sendMessage(Message msg)boolean sendMessageAtFrontOfQueue(Message msg)boolean sendMessageAtTime(Message msg, long uptimeMillis)boolean sendMessageDelayed(Message msg, long delayMillis)
- 插入简单数据对象到消息队列
boolean sendEmptyMessage(int what)boolean sendEmptyMessageAtTime(int what, long uptimeMillis)boolean sendEmptyMessageDelayed(int what, long delayMillis)
- 插入任务到消息队列
boolean post(Runnable r)boolean postAtFrontOfQueue(Runnable r)boolean postAtTime(Runnable r, Object token, long uptimeMillis)boolean postAtTime(Runnable r, long uptimeMillis)boolean postDelayed(Runnable r, long delayMillis)
MessageQueue
里只能放Message
对象,那是如何传递Runnable
的呢?我们来看一下源码:
// handler调用post方法发送任务public final boolean post(Runnable r) { return sendMessageDelayed(getPostMessage(r), 0);}// 将Runnable包装成messageprivate static Message getPostMessage(Runnable r) { Message m = Message.obtain(); m.callback = r; return m;}
通过发送任务的方法源码我们知道,其实最终加入消息队列的还是消息对象,只是将任务进行了包装。
总结
基本将Handler
、Message
、MessageQueue
、Looper
协作运转的过程简单走了一遍,但是还是有些东西没讲,如消息的生命周期、消息队列的空闲时间、自定义callback
、移除消息、消息处理过程跟踪等。但是这个简析基本理清了他们之间的关系以及工作原理,在使用上的话应该不会存在大问题了。
更多相关文章
- Android(安卓)App启动过程
- Accessibility辅助功能的使用
- windows下配置安卓开发环境
- android大分辨率图片的缩放处理和图片添加水印+文字,水印图片对角
- 【Android】查看程序每个方法所花费的时间
- Android(安卓)常用设计模式(二) -- 单例模式(详解)
- Android(安卓)多进程通信(2) - Binder、Messenger
- Android如何分析和研究Monkey Log文件
- Android:自定义View(一)