今天我们来了解一下Handler。Android中操作UI控件需要在主线程中进行,为了打破对主线程的依赖(将耗时操作在后台线程执行,而将执行结果在ui线程中操作ui显示),Android引入了Handler消息传递机制。
Handler
Handler有这样三个特点:
a.允许你去发送并处理一条与Runnable对象和MessageQueue相关联的消息。
b.每一个Handler实例都与一个单独的线程和该线程的MessageQueue相关。
c.当你创建一个新的处理程序时,它将绑定到正在创建的线程的线程/消息队列——从那个点开始,它将向该消息队列传递消息和runnables,并在它们从消息队列中释放时执行它们。
实际上,根据我的理解,handler就是我们在各线程间处理发送消息数据的一种机制,实现线程间切换的一种方式。
Handler有两个主要用途:
(1)将消息和runnables作为将来的某个点执行
。
(2)在不同的线程上执行要执行的操作
。
Handler线程模型
先介绍了一下Handler的基本概念,下面就来说一下handler线程模型与一些handler常用的函数以及相关的类。
- 我们都知道,android的UI操作必须要在主线程中进行,为什么?
因为在多线程中同时执行UI操作是不安全的
- 可以把全部的操作都放在主线程中么?
不可以,耗时操作需要在后台线程执行,避免ANR
也就是说,如果我们进行了耗时操作,如网络加载图片后,又想显示在我们的ImageView上,那么就需要进行线程间切换,使用handler将后台线程切换到主线程上后,进行UI操作。
Handler常用方法
构造方法
Handler是Android中实现线程间切换机制的类
- public Handler() 无参构造,直接new。
// 若只是执行runnable则不需要覆写handleMessage方法。因为执行runnable会自动忽略handleMessage方法本身。
Handler handler0 = new Handler();
// 如果我们要对发送的消息进行操作则需要覆写handleMessage方法。
Handler handler1 = new Handler(){
@Override
public void handleMessage(Message msg) {
// 获得消息后操作
}
}
- public Handler(Callback callback)
- public Handler(Looper looper)
- public Handler(Looper looper,Callback callback) 参数1,looper;参数2,callback回调函数,相当于实现handler抽象方法handleMessage(Message msg);。
public interface Callback {
public boolean handleMessage(Message msg);
}
调用函数
- post(Runnable r) 将Runnable添加到MessageQueue中。此处要强调一点,如果发送的是runnable则会忽略掉handleMessage方法的执行,即使是发送含有runnable的Message则也会忽略掉handleMessage方法的执行
public interface Callback {
public boolean handleMessage(Message msg);
}
- postAtTime(Runnable r, long updateMillis) 在指定的时间点运行runnable。
- postDelayed(Runnable r, long delayMillis) 在延迟一段时间后运行runnable。
// 此处两个方法执行runnable时机一致
handler.postDelayed(runnable,2000);
handler.postAtTime(runnable,System.currentTimeMillis() + 2000);
- postAtFrontQueue(Runnable r) 将Runnable放在队列最前端执行
- sendEmptyMessage(int what) 发送一个空消息,参数为int型what(消息执行标识)。
- sendMessage(Message msg) 发送消息。
- sendMessageAtTime(Message msg,long updateMillis) 在指定时间点发送消息。
- sendMessageDelayed(Message, long delayMillis) 在延迟一段时间后发送消息。
- sendMessageAtFrontOfQueue(Message msg) 将message放在消息队列的最前面发送。
sendMessage方法与post方法最后执行handleMessage方法或执行runnable的线程就是Handler的创建线程。
取消任务
- removeCallbacks(Runnable r) 移除当前消息队列中runnable == r的消息。
- removeCallbacks(Runnable r ,Object token) 移除当前消息队列中target == handler,runnable == r,Object == token 的消息。
- removeMessages(int what) 取消所有标识为what的消息。
- removeMessages(int what ,Object token) 取消含有token对象的被what标识的消息。
- removeCallbacksAndMessages(Object token) 取消含有token的全部消息。
有关Handler常用类,四个组成部分
- Message:消息,被传递和处理的数据。其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,终由Handler处理。
Message作为传递的消息与Handler密不可分,所以此处我们稍微展开讲解一下Message属性与获得方式:
Message的属性):
- public int what 传递的消息执行标识,用于标记不同的消息。
- public int arg1 类似与bundle来进行携带int类型的数据。
- public int arg2 如果一个arg1还不够用再来个arg2。
- public Object object 任意对象传递容器。类似于bundle,用于跨进程专递对象
- Runnable callback 执行的runnable对象。
- long when 发送的时机。
- Handler target 所依附的handler,可以理解为通过这个target,这个消息会找到它该去的地方,然后执行对消息的操作也就是handleMessage方法。
Message的获得(通常使用Message.obtain()或Handler.obtainMessage()方法来从消息池中获得空消息对象,以节省资源): - Message msg0 = new Message();
- Message msg1 = Message.obtain();
- public static Message obtain(Handler h)
- public static Message obtain(Handler h, int what)
- public static Message obtain(Handler h, int what, Object obj)
- public static Message obtain(Handler h, int what, int arg1, int arg2)
- public static Message obtain(Handler h, int what, int arg1, int arg2, Object obj) 参数1:targetHandler,参数2:what标识符,参数3:int型数据,参数4:int型数据,参数5:Object对象。
- public static Message obtain(Message orig) 相当于拷贝某个Message而获得Message。
- public static Message obtain(Handler h,Runnable callback) 传入targetHandler与需要回调执行的runnable。
- Handler:处理者,负责Message的发送及处理。使用Handler时,需要实现handleMessage(Message msg)方法来对特定的Message进行处理,例如更新UI等。Handler类的主要作用:(有两个主要作用)
a. 在工作线程中发送消息;
b. 在主线程中获取、并处理消息。 - MessageQueue:消息队列,本质是一个数据结构,用来存放Handler发送过来的消息,并按照FIFO规则(先进先出)执行。当然,存放Message并非实际意义的保存,而是将Message串联起来,等待Looper的抽取。
- Looper:消息泵或循环器,不断从MessageQueue中抽取Message。因此,一个MessageQueue需要一个Looper。
-
Thread:线程,负责调度整个消息循环,即消息循环的执行场所。
此处我们画一个简易图来逐步了解Handler的运行机制。图中Handler(左右两个handler为同一个handler)可以与MessageQueue以及Looper在同线程中,也可在不同线程中。
Handler进程间切换举例
到这里,我想我们脑中有了一个对Handler大致的理解,渐渐的看清了它的样子,那么接下来,我们来写一些代码,来看看handler究竟是怎样运行,工作的。
主线程创建handler
为了节约版面,我们不列举每一种方法,只举例有代表性的方法,其他的都会提交到我的github中Handler传送门,大家有需要可以去下载研究。
- 我们先在主线程中创建handler而在非主线程中来调用post(Runnable r),观察是否最后runnable运行的线程为主线程(代码段中post(View v)方法为button点击调用函数)。
public void post(View v) {
Log.e("tag", " ------>>> " + "点击Post按钮 " + getThreadName());
// 在主线程创建handler
final Handler handler = new Handler();
// 创建一个非主线程的新线程
Thread thread = new Thread(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "执行非主线程runnable " + getThreadName());
// 此处可以进行一些耗时操作,比如加载网络图片或读写文件等操作。
// handler.post(runnable) 去观察是否回到主线程
handler.post(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "post runnable" + getThreadName());
}
}
);
}
}
);
// 运行该线程
thread.start();
}
运行结果:
05-29 14:58:54.234 6268-6268/com.perry.handler E/tag: ------>>> 点击Post按钮 主线程
05-29 14:58:54.238 6268-6320/com.perry.handler E/tag: ------>>> 执行非主线程runnable 非主线程
05-29 14:58:54.253 6268-6268/com.perry.handler E/tag: ------>>> post runnable 主线程
可以看到我们的handler成功的完成了线程间切换的任务,将需要执行的runnable在非主线程进行耗时操作后回到handler所创建的线程也就是主线程运行。
- 再来看一下sendMessage(Message msg)方法,后执行handleMessage(Message msg)方法的方法体,是否完成了线程间切换。
public void sendMessage(View view) {
Log.e("tag", " ------>>> " + "点击sendMessage按钮 " + getThreadName());
final Handler handler = new Handler() {
// 此处处理msg需要覆写该方法
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
Log.e("tag", " ------>>> " + "sendMessage" + getThreadName() + " 参数 = " + msg.obj);
break;
}
}
};
// 创建一个非主线程的新线程
Thread thread = new Thread(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "执行非主线程runnable " + getThreadName());
// handler.sendMessage(msg) 去观察是否回到主线程
handler.sendMessage(Message.obtain(handler, 0, "obj参数"));
}
}
);
// 运行该线程
thread.start();
}
运行结果:
05-29 15:08:53.077 6682-6682/com.perry.handler E/tag: ------>>> 点击sendMessage按钮 主线程
05-29 15:08:53.078 6682-6729/com.perry.handler E/tag: ------>>> 在非主线程执行 handler.sendMessage(msg) 非主线程
05-29 15:08:53.096 6682-6682/com.perry.handler E/tag: ------>>> sendMessage 参数 = obj参数 主线程
可以看到我们的sendMessage(Message msg)方法也成功的完成了线程间切换的任务,将需要执行的对msg的操作在非主线程进行耗时操作后回到主线程运行handleMessage(msg)的方法体。
- 之前说过如果发送的是runnable则会忽略掉handleMessage方法的执行,即使是发送含有runnable的Message则也会忽略掉handleMessage方法的执行,那么来证明一下。
public void sendMessageCallback(View view) {
Log.e("tag", " ------>>> " + "点击sendMessageCallback按钮 ");
final Handler handler = new Handler() {
// 此处处理msg需要覆写该方法
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// handleMessage 执行方法体
Log.e("tag", " ------>>> " + "sendMessage handleMessage");
}
};
// 发送一个Message
handler.sendMessage(
// 创建含有Runnable callback的msg
Message.obtain(
handler,
new Runnable() {
@Override
public void run() {
// callback 执行方法体
Log.e("tag", " ------>>> " + "sendMessage Runnable");
}
}
)
);
}
// 执行结果
05-31 14:36:54.975 2083-2083/com.perry.handler E/tag: ------>>> 点击sendMessageCallback按钮
05-31 14:36:54.992 2083-2083/com.perry.handler E/tag: ------>>> sendMessage Runnable
为了看的更清晰,这里去掉了线程的切换与打印,都是在主线程里发送和接收的消息,我们最终发现,执行了runnable的方法体,未执行handleMessage的方法体,所以只要有runnable的存在就会忽略handleMessage方法体的执行。
说到这为止,我们都是从ui线程创建handler最后返回ui线程执行,那如果我们不想在ui线程执行呢?怎么办?
后台线程创建handler
前面在介绍handler时由一句话sendMessage方法与post方法最后执行handleMessage方法或执行runnable的线程就是Handler的创建线程。所以先写一个在后台创建的handler试试水,看看会不会如我们期待一样,回到后台线程执行。
public void postBackgroundThread(View view) {
Log.e("tag", " ------>>> " + "点击postBackgroundThread按钮 " + getThreadName());
Thread thread = new Thread(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "执行非主线程runnable " + getThreadName());
Handler handler = new Handler();
handler.post(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " +"postBackgroundThread " + getThreadName());
}
}
);
}
}
);
thread.start();
}
运行一下,结果发现,程序崩溃,讲道理我们就是在非UI线程创建了个handler然后post了一下,为什么会崩溃呢?日志如下:
05-30 15:26:07.446 711-711/com.perry.handler E/tag: ------>>> 点击postBackgroundThread按钮 主线程
05-30 15:26:07.486 711-773/com.perry.handler E/tag: ------>>> 执行非主线程runnable 非主线程
05-30 15:26:07.490 711-773/com.perry.handler E/AndroidRuntime: FATAL EXCEPTION: Thread-18868
Process: com.perry.handler, PID: 711
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:209)
at android.os.Handler.<init>(Handler.java:123)
at com.perry.handler.OnBackgroundActivity$2.run(OnBackgroundActivity.java:71)
at java.lang.Thread.run(Thread.java:818)
到这里,我们可以清楚的看到,不能在非主线程中创建handler时不调用Looper.prepare()方法。接下来,就看看,如何正确的在非主线程中创建handler。
1.手动进行Looper.prepare()及Looper.loop()操作。
public void postNoCrash(View view) {
Log.e("tag", " ------>>> " + "点击postNoCrash按钮 " + getThreadName());
Thread thread = new Thread(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "执行非主线程runnable " + getThreadName());
Looper.prepare();
Handler handler = new Handler();
handler.post(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "postNoCrash " + getThreadName());
// 使用后需要退出looper避免线程阻塞
Looper.myLooper().quit();
}
}
);
Looper.loop();
}
}
);
thread.start();
}
// 执行Log日志
05-30 16:40:14.660 6368-6368/com.perry.handler E/tag: ------>>> 点击postNoCrash按钮 主线程
05-30 16:40:14.672 6368-6426/com.perry.handler E/tag: ------>>> 执行非主线程runnable 非主线程
05-30 16:40:14.694 6368-6368/com.perry.handler E/tag: ------>>> postNoCrash 非主线程
2.利用HandlerThread类进行创建。HandlerThread继承Thread,它会启动一个带有looper的新线程,这样我们就可以在非UI线程中创建handler,但是必须要调用start()方法。
public void handleThread(View view) {
final HandlerThread handlerThread = new HandlerThread("HandlerThread");
handlerThread.start();
Log.e("tag", " ------>>> " + "点击handleThread按钮 " + getThreadName());
Thread thread = new Thread(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "执行非主线程runnable " + getThreadName());
Handler handler = new Handler(handlerThread.getLooper());
handler.post(
new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " +"handleThread " + getThreadName());
}
}
);
}
}
);
thread.start();
}
// 执行Log日志
05-30 16:40:23.301 6368-6368/com.perry.handler E/tag: ------>>> 点击handleThread按钮 主线程
05-30 16:40:23.311 6368-6428/com.perry.handler E/tag: ------>>> 执行非主线程runnable 非主线程
05-30 16:40:23.323 6368-6368/com.perry.handler E/tag: ------>>> handleThread 非主线程
我们的handler的线程间切换的方法介绍的差不多了,无论是在主线程还是在非主线程中进行线程间切换,而且也印证了我们那句话sendMessage方法与post方法最后执行handleMessage方法或执行runnable的线程就是Handler的创建线程。但是还有一个小东西我们要介绍一下,就是MessageQueue的IdleHandler()接口。
IdleHandler()接口
该接口的用途是MessageQueue通过实现该方法,将queueIdle()方法的方法体,添加到消息队列里面去,而不使用handler就可以实现。
public void idleHandler(View view) {
Log.e("tag", " ------>>> " + "点击idleHandler按钮 " + getThreadName());
Looper.myQueue().addIdleHandler(
new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.e("tag", " ------>>> " + "执行非主线程runnable " + getThreadName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("tag", " ------>>> " + "idleHandler " + System.currentTimeMillis());
makeToast("IdleHandler " + getThreadName());
return false;
}
}
);
}
IdleHandler()是在消息队列空闲的时候,也就是说所有的其他消息是需要延时执行时,会执行该方法的方法体。返回值为false时则在空闲的时候会执行一次该方法体,执行一次后不会再执行,而返回true时,若执行一次后,消息队列依旧空闲则还会再执行该方法体。IdleHandler()是在消息队列空闲的时候执行的,所以在当IdleHandler()执行之后的方法,比如是在1s延迟后执行,但是IdleHandler()在这段时间内被执行了而需要耗时2s,则它之后的消息要在2s之后才执行而不是原有的1s之后执行。所以,在IdleHandler()方法中不要做太耗时的操作。
为了举例证明,IdleHandler()是在消息队列空闲的时候执行的,我们这里先点击了sendMessageDelayed按钮延时1s进行打印log,然后马上点击了idleHandler按钮其中做了2s线程休眠,对比一下执行时间log,结果如下:(想要自己手动实验的小伙伴Handler传送门送给你,可以给我个小星星!)
05-30 17:05:20.344 9685-9685/com.perry.handler E/tag: ------>>> 点击sendMessageDelayed按钮 1527671120344
05-30 17:05:20.828 9685-9685/com.perry.handler E/tag: ------>>> 点击idleHandler按钮 主线程
05-30 17:05:20.836 9685-9685/com.perry.handler E/tag: ------>>> 执行非主线程runnable 主线程
05-30 17:05:22.881 9685-9685/com.perry.handler E/tag: ------>>> IdleHandler 主线程
05-30 17:05:22.881 9685-9685/com.perry.handler E/tag: ------>>> idleHandler 1527671122881
05-30 17:05:22.885 9685-9685/com.perry.handler I/Choreographer: Skipped 121 frames! The application may be doing too much work on its main thread.
05-30 17:05:22.923 9685-9685/com.perry.handler E/tag: ------>>> sendMessageDelayed 参数 = obj参数 主线程
05-30 17:05:22.923 9685-9685/com.perry.handler E/tag: ------>>> sendMessageDelayed方法体执行 1527671122923
取消任务
如何发送消息我们都列举完了,接下来我们来取消任务。
public void removeCallbacks(View view) {
Log.e("tag", " ------>>> " + "removeCallbacks按钮 " + getThreadName());
Handler handler = new Handler();
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.e("tag", " ------>>> " + "postDelayed removeCallbacks " + getThreadName());
}
};
handler.postDelayed(runnable, 1000);
handler.removeCallbacks(runnable);
}
// log日志
05-31 11:27:18.791 25879-25879/com.perry.handler E/tag: ------>>> removeCallbacks按钮 主线程
这里点击按钮后只显示了一个点击removeCallbacks按钮的log,即使再等了10s也仍未见到runnable中的log日志,成功的移除了该runnable任务。而其他的remove方法也都大同小异,不过是更详细的定位了,到底是哪个runnable或Message,想了解的小伙伴请看Handler传送门。
退出Looper
后台线程不需要的时候,这时后台looper我们也不需要了,或者说我们在某个事件后或某个时间点不需要后续的消息进行发送到消息队列中或执行消息,我们可以通过handlerThread.quit()方法或handlerThread.quitSafely(),方法来退出looper。
public void handlerThreadQuit(View view) {
Log.e("tag", " ------>>> " + "handlerThreadQuit按钮 " + getThreadName() + System.currentTimeMillis());
final HandlerThread handlerThread = new HandlerThread("HandlerThread");
handlerThread.start();
final Handler handler = new Handler(handlerThread.getLooper());
Thread t = new Thread(
new Runnable() {
@Override
public void run() {
// 首先延迟1s发送一条消息
handler.postDelayed(
new Runnable() {
@Override
public void run() {
makeToast("postDelayed 1s runnable");
}
}, 1000
);
// handler执行runnable进行退出looper
handler.post(
new Runnable() {
@Override
public void run() {
handlerThread.quit();
makeToast("HandlerThread Loop quit");
}
}
);
}
}
);
t.start();
}
quit方法执行后handler将不再接收和执行任何消息,而quitSafely执行后,会先看消息是否过线,如图Handler简易关系.png中消息是否可派发分割线,不可派发为未过线,可派发为过线,过线的消息则可继续执行,未过线的消息则不可执行。且执行quitSafely方法需要在API18以上。
自定义Handler
写了这么多但回头一看发现,很多方法内部报黄。原来是因为handler内部持有外部类的引用,造成内存无法释放而泄漏。那如何解决呢?这就需要我们来自定义Handler了。
此处我们只要用弱引用,引用我们所需要的资源,就不会发生泄漏了,具体代码见Handler传送门,记住界面销毁要适时的取消任务哦!
在平时的使用中,只要你认真的看过了这篇文章并动手稍加实践,那一定可以很好的掌握handler的原理及用法,若想要深入学习了解,就需要去分析handler的源码,也就是我的下一篇文章handler源码分析。
下一篇:Handler源码分析。