Handle消息机制

概要

首先我们说下我们为什么要设计一个handle?

现在出个题,如果没有handler你在2个线程之间如何同步数据?
实现这势必就需要弄锁机制,但是只有主线程才可以更新ui,这个时候上锁阻塞就非常影响主线程,接着,如果能够线程通信,数据的传送是不是需要依次过来,队列就来了。类似一种生产者消费者模型。Handler的最大作用就是线程的通信,并不是线程切换。

基本认知

先对handle的一个基本认知,我们知道,我们的程序应用开启流程是由一个launch(app)去生产一个zygote,然后zygote去孵化一个独立的jvm。然后在这个jvm开启了我们的应用,应用被开起第一个被调用的就是
AcitivityThread.java 里面的main函数,而在这个mian函数里面为当前主线程做了
Looper.prepareMainLooper();,以及Looper.loop();
并且是不允许主线程Looper.quit(),不然应用就退出了。

很多个应用即为很多个进程,所以有IPC多进程进行通讯,可以用Binder 或者socket ,socket可以在两个终端上进行通讯,也可以在同一个终端两个进程进行通讯,而进程又分为主线程和子线程,一个进程至少要一个线程,那个线程就是主线程,也叫UI线程。

问:为什么只可以ui线程更新ui?给加上sync锁不就好?

答:UI控件非线程安全,在多线程并发访问可能会导致ui控件处于不可预期的状态,而不能对ui控件的访问加sync锁是因为,上锁会让ui控件变得复杂和低效,上锁会阻碍某些进程的执行。有时候一些网络请求和数据输入输出及耗时任务在主线程弄会太慢导致阻塞,导致给UI线程慢,给用户带来不好效果,所以耗时任务都再开启一个线程 去做。在android中,只有在主线程才可以更新UI,在子线程是不可以的。
那么为了解决这种问题,Android 提供了handle消息机制这种东西。
而handle消息机制并非只是为了解决更新ui问题,这只是它的一个使用场景。Handler的最大作用就是线程的通信,并不是线程切换。

ThreadLocal

首先啥也先别说,先说一个叫ThreadLocal ,理解了他,对 handle的消息机制才能懂。

ThreadLocal通常称为“线程局部变量”,也就说某些数据是以线程为作用域,在不同线程中有不同的数据副本。简单来说,就是每个线程对应一个值,你在这个A线程只能存A线程的数据和得到A线程的数据,不能得到B线程的,有点像hashmap。
下面通过例子来说明,首先定义一个 ThreadLocal 对象,选择 Boolean 类型,如下所示

private ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<>();

然后分别在主线程、子线程1和子线程2中设置和访问它的值

private void threadLocal() {mThreadLocal.set(true);Log.d(TAG, "[Thread#main]threadLocal=" + mThreadLocal.get());new Thread() {@Overridepublic void run() {super.run();mThreadLocal.set(false);Log.d(TAG, "[Thread#1]threadLocal=" + mThreadLocal.get());}}.start();new Thread() {@Overridepublic void run() {super.run();Log.d(TAG, "[Thread#2]threadLocal=" + mThreadLocal.get());}}.start();}

虽然在不同的线程中访问的是同一个 ThreadLocal 对象,但是通过 ThreadLocal 获取到的值是不一样的。
之所以会这样,我们来说一下它的工作原理。

说它原理前,我们来说一下准备知识。

public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}private Entry[] table;}

也就是每一个线程都有一个

ThreadLocal.ThreadLocalMap threadLocals ,而threadLocals 内部有个table[]数组

ok 准备知识结束。继续说ThreadLocal 工作原理。
其实就是ThreadLocal 在调用set()方法的时候,set()里面去获取当前线程,然后在根据当前线程的
ThreadLocal.ThreadLocalMap threadLocals ,把ThreadLocal 的引用对像,跟要放数据,一起放在这个线程的 ThreadLocal.ThreadLocalMap threadLocals的table[]数组中,reference的索引位置加1就是value的索引位置,也就是类似
table[0]=ThreadLocal 引用 ,table[1]=value.
这样,每个线程就可以放n个ThreadLocal 引用,和n个value,对应关系就是那个+1的映射关系。而ThreadLocal的gei()方法也是一样,先
得到当前线程,然后在根据当前线程的 ThreadLocal.ThreadLocalMap threadLocals 的table[]数组,根据哪个个ThreadLocal的引用在table[]数组位置得到相应的value值。

总结,也就是说,每个线程都有个ThreadLocal.ThreadLocalMap threadLocals,而这个ThreadLocal.ThreadLocalMap threadLocals有个table[]数组,当一个ThreadLocal 对象引用 在一个线程里面set的时候就会把当前这个对象引用和 set的值放在当前线程的table[]数组里面,reference的索引位置加1就是value的索引位置,也就是类似table[0]=ThreadLocal 引用 ,table[1]=value, 那么当这个ThreadLocal 引用再次调用get的时候,就会从当前线程的table[]数组里面 根据这个引用获取 对应的值。这样,创建new了n ThreadLocal ,做了set 和get ,个每个线程就可以放n个ThreadLocal 引用,和n个value。
所以 同一个 ThreadLocal 对象,在不同的线程 set 不同的值 , 在不同的 线程 get 出来值 是不一样的 。

Handle消息机制

先看看它的基本使用

Handle操作基本有2种。

第一种就是用handleMessage,步骤为在开启的线程中做耗时任务,需要改变UI的时候就发送消息出来,然后再主线程handler里去改变,handler里也是主线程包含在内的。
类似源代码类似如下:

android.os.Handler handler =new android.os.Handler(){//注意 Handler 包要用os的@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);//收到消息要做的更改UI的操作,msg是收到的消息}};//收到消息改变UIButton button = (Button) findViewById(R.id.c);button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {handler.sendEmptyMessage(0x123);//开启线程做耗时工作,需要改变UI就发送信息给主线程的handler去做}});thread.start();}});

在这里边,创建Handler的线程和其handleMessage运行的线程是同一线程,mHandler是在主线程中创建的,所以其handleMessage方法也是在主线程中运行。mHandler.sendMessage(Message)可以在任何线程,
sendMessage还有许多变形,可以发送空message(只携带what参数)、延时消息、定时消息等。使用方式很简单。

第二种 采用handler.post

Handler handler =new Handler();button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {new Thread(new Runnable() {@Overridepublic void run() {handler.post(new Runnable() {@Overridepublic void run() {textView.setText(R.string.sample);}});}}).start();}});

Handler还是在主线程中创建,然后post方法参数为Runnable对象,所以Runnalbe会在主线中运行(与Runnable创建的线程无关、与mHandler.post方法调用的线程无关)。

也有一种方便的集成函数,叫做 runOnUiThread,即在一个子线程里调用showResponse(finalResult); 这个函数,finalresult是把子线程的控件需要的数据拿到主线程中,再在主线程中,

 public void showResponse(final String s) {runOnUiThread(new Runnable() {@Overridepublic void run() {mTextView.setText(s);}});}

好了接下来我们来说handle的消息机制。

首先
looper是个管家,负责处理消息,messagequeue是个队列,是个单链表的数据结构,存放着Message。

Message类携带很多参数,

public int what;
public int arg1;
public int arg2;
public Object obj;

根据需要设置Message的参数,Message.what一般都是必要的,用来区分不同的Message,做出不同的操作。还可以设置Message两个int型字段arg1、arg2。当然除了这简单的数据外,还可以设置携带复杂数据,其obj字段类型为Object类型,可以为任意类类型的数据。也可以通过Message的setData方法设置Bundle类型的数据,可以通过getData方法获取该Bundle数据

  Message = new Message();msg.what = 1;msg.arg1 = 111;  可以设置arg1、arg2、obj等参数,传递这些数据msg.arg2 = 222;String zjs = "zjs";msg.obj = zjs;//String a= (String) msg.obj;Bundle bundle= new Bundle();bundle.putString("name","zjs");msg.setData(bundle);// msg.getData().getBundle("name");//msg .target为信息的handler对象,即这条信息是谁发送的。

handle 负责send message,到messagequeue中去,handle发送有两种,一种是 post 了runable对象,一种是send 一个message 都到
messagequeue中,其实handle发送 post 了runable对象,其实还是调用了send 方法,这只是一种对runable的封装。看如下源码:

 public final boolean post(Runnable r){return  sendMessageDelayed(getPostMessage(r), 0);}
 public final boolean sendMessage(Message msg){return sendMessageDelayed(msg, 0);}

可见,两个方法都是通过调用sendMessageDelayed方法实现的,所以可以知道它们的底层逻辑是一致的。
但是,post方法的底层调用sendMessageDelayed的时候,却是通过getPostMessage®来将Runnable对象来转为Message,我们点进getPostMessage()可以看到:

 private static Message getPostMessage(Runnable r) {Message m = Message.obtain();m.callback = r;return m;}

从上面可以知道 。post个runable其实还是send 个message,其实就是把 runable 放在message.callback 中 ,即
message.callback=runable
然后最终还是send 个message对象到messagequeue

这么做的含义:
为了更方便开发者根据不同需要进行调用。当我们需要传输很多数据时,我们可以使用sendMessage来实现,因为通过给Message的不同成员变量赋值可以封装成数据非常丰富的对象,从而进行传输;当我们只需要进行一个动作时,直接使用Runnable,在run方法中实现动作内容即可。

好了接下来我们来看看流程。

首先,一个线程只有一个looper,和一个messagequeue,一个线程可以有多个handler,当这些handle被new的时候,它们就已经跟looper,messagequeue和当前线程绑定在一起,之后,handle不管在其他哪个线程post还是send(其实根据上面我们知道都一样),都会最终传到handle被创键线程中。具体流程如下:
handle1 在线程a 创建,然后在线程b或者其他线程 send message,那么这个消息就会把当前这个handle1附加在这个message.target身上
也就是 message.target = handle1。也就是说。looper从messagequeue拿消息是根据这个消息的message.target知道等下要发送给哪个handle的

那么线程a 中的messagequeue 的enqueuemessage就会被调用,enqueuemessage是一个往messagequeue 插入一条message的方法,最主要就是messagequeue 有个next()方法,next 会一直在队伍里面循环找消息,有就返回这条消息给looper,然后把这条消息从队伍中删除,没有就阻塞等待。

注意messagequeue 的数据结构是由单链表实现的优先级队列,
为什么这么说呢
我们知道,messagequeue 是一个装载着一个个Mesaage 里货梯,而Mesaage 类里有一个变量叫做Mesaage next。也就是 ,messagequeue 里面的mesaage们,是由一个mesaage 的next指向下一个mesaage 的,
mesaage ->next->mesaage ->next->message
这种就叫单链表,单链表就是next只向单方向的下一个索引,如果是双链表,就双方向的。

并且每个mesaage都有它的对应操作时间,也就是,不管handle 是延迟发送一个mesaage,还是不延迟,发送的mesaage都有它的一个对应时间,比如当handle 第一次发送一个mesaage,延迟时间是10s到messagequeue ,当下一个mesaage,延迟时间是5s,进入到messagequeue ,她会对messagequeue 里面的所有mesaage做一个遍历,哪个需要早操作就放前面,然后把5s的mesaage排在最前面。

messagequeue 的个next()方法是需要Loopr.looper()这个方法开启的,也就是Loopr才是开启的死循环寻找消息的源头,唯一跳出这个循环 是looper.quit(),looper.quit()后,messagequeue 的个next()方法会返回个null,然后两者死循环结束,这时候就全部结束,handle send ()就会返回fasle。looper 还有一种退出的方法是looper.quitsafely(),looper.quit()是立即结束,looper.quitsafely()是等队伍中消息做完再结束。所以是实现开发的时候,不用的工作了就应该,looper.quit()或者looper.quitsafely(),不然死循环寻找消息会一直。接下继续,looper拿到messsage后,就会调用message.target.dispatchMessage方法
也就是会根据这个的message.target知道这个消息是属于哪个handle的 ,接着会调用对于handle的dispatchMessage()方法,我们看下源码;
dispatchMessage

public void dispatchMessage(Message msg) {if (msg.callback != null) {// 如果有Runnbale,则直接执行它的run方法handleCallback(msg);} else {//如果有实现自己的callback接口if (mCallback != null) {//执行callback的handleMessage方法if (mCallback.handleMessage(msg)) {return;}}//否则执行自身的handleMessage方法handleMessage(msg);}
}private static void handleCallback(Message message) {message.callback.run();
}

根据源码我们清楚得到,先是判断这message是否是带着callback ,即runable,是那么去run,然后结束。不然的话,判断再创建handle 的方式是否用callback的方式,即:

  Handler.Callback callback= new Handler.Callback() {@Overridepublic boolean handleMessage(Message msg) {//处理消息return false;}};Handler handler= new Handler(callback);handler.sendEmptyMessage(123);

平时我们创建handle是派生一个handle子类然后重写handleMessage,即

android.os.Handler handler1=new android.os.Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);}};handler1.sendEmptyMessage(123);

创建方式的不同,有个区别就是前后顺序不一样,如果Callback的handleMessage返回false,则后面的可以正常执行。但当返回true时,Message就被截获了,后面的handleMessage将不会被执行。

所以dispatchMessage先是判断这message是否是带着callback ,即runable,是那么去run,然后结束。不然的话,判断再创建handle 的方式是否用callback的方式,有就去调用callback的handleMessage,没有就调用的handleMessage()。

好了具体流程就这么结束了,接下来看看一些其他应用知识。

我们知道,handle被创建的时候,会跟当前线程的looper构成消息循环系统,那么handle是如何找到当前线程的looper呢,而每个线程为什么只有一个looper,其实就是利用上面的Threadlocal知识,Threadlocal可以再不同线程中互不干扰的存储并提供数据,再在handle这里,存储的数据就是looper,Looper保存在ThreadLocal里面的,源码保证只可以有一次set,也就是只有一个ThreadLocal引用,所以,也就只有一个looper,通过Threadlocal,就可以轻松获取每个线程的looper,当然 每个线程是没有looper的,需要自己创建,即:

Looper.prepare();//
Looper.loop()//开启循环

而主线程中,默认已经自动创建了looper和messagequeue,所以handle在主线程创建,就跟他们绑定在一起,如果在子线程创建handle,就要设置主线程的looper或者创建子线程Looper.prepare();(需要哪个线程的就用哪个咯)给handle,不会报异常。

问:既然可以存在多个handle往一个messagequeue插入消息的,handle又可能处于不同个线程,那她内部是如何保证线程安全的,

答:messagequeue里的queuemessage方法用
synchronized(this)也就是拿了messagequeue的对象锁,也就是
一个线程只有一个messagequeue,那么那么多个handle做的插入操作,谁先拿到了messagequeue对象锁,谁就插入操作,造成同步导线程安全

我们要知道,handle消息机制并不是线程切换,所谓的线程切换是A线程post消息之后后面的代码不执行了,切换到B线程执行,B线程执行完再回到A线程。 但是,handle并不是这样,handle是线程通信,也就是A线程post消息之后后面的代码继续执行了,并不i会切换到b线程,只是通信让B线程的处理消息的代码去做。

我们创建一个message 应该以什么方式?

创建一个message 可以用new ,但是最好用
Message msg = Message.obtain(); 获得一个消息。

假设当一个占用10m大小的箱子message被处理完后,它占用的内存位置和大小并不会被回收,而是把这块箱子区域的内容置空,然后放到缓存池中,方便下次复用。我们用Message.obtain()方法获取一个消息时,会先从缓存池看是否有我要的箱子大小,如果缓存池没有消息,才会去创建消息。这样做可以做的目的是为了防止内存碎片造成的内存抖动引起的oom

这样做的原因是,我们知道,我们每一次new 出一个对象,都会在应用内存总大小中随机位置抠出一块区域,而如果在短时间内,又不断的扣出很多大大小的的洞,尽管会gc回收,在这样短时间内不断创建和回收的同时,就会出现很多内存碎片,从而造成内存抖动,而如果突然需要一个极大的占用内存大小的箱子,而内存占用大小又连续的,尽管内存中是有足够的大小给予的但是扣了很多个位置导致没有连续大位置所以就是oom。所以用Message.obtain();会先去看有没有适合的箱子已经存在,没有再去new对象。

同一个Message不要发送两次。如下面的代码是有问题的:

//同一个Message发送了两次
Message msg = Message.obtain();
handler.sendMessage(msg);
handler.sendMessage(msg);

这是因为所以的消息都是发送到MessageQueue存放,然后等待处理的。如果一个Message对象在MessageQueue里,再次把它存到MessageQueue时就会报错。

问:我们从上面知道主线程就有个looper,而looper一直在做死循环,那么为什么不会造成应用卡死即anr。

这两者没有关系,anr是当在固定时间内消息未处理就会造成,也就是比如looper在主线程中死循环执行一个消息耗时太久,而造成队列中其他消息事件没有得到相应就会造成anr,而looper.looper死循环是队列中没有消息,它阻塞睡眠了,当一有消息来它立马被唤醒了。这两者是两回事。

同步屏障
上面中我们知道,线程的消息都是放到同一个MessageQueue里面,并且是按照每个信息的对应的时间排列起来的顺序,looper从MessageQueue拿信息并不需要遍历所有信息,它只需要拿第一个,因为队列中第一个就是最早需要处理的消息,那么问题来了,MessageQueue的信息们是同步的一个个执行的,那么同一个时间范围内的消息,如果有一个消息它需要立刻执行,那我们怎么办,做法思想就是对任务队列中插入一个同步屏障,然后把这个需要立刻处理的信息设置为是异步的,这样当looper从MessageQueue的队列中第一个信息发现了这个同步屏障,那么他就去遍历,在队列中找到这个异步的消息并且排斥了同步的消息,做完异步的消息后,再撤销对这个同步屏障。

设置一个同步屏障是利用

MessageQueue的postSyncBarrier这个方法。
而设置一条信息为异步的是调用

Message message=Message.obtain();
message.setAsynchronous(true);
handler.sendMessage(message);

接着最后撤销掉这个同步屏障,调用的是

MessageQueue的removeSyncBarrier这个方法

那么他们的原理是什么呢?
一开始,当你设置信息为异步消息,然后调用MessageQueue的postSyncBarrier,在postSyncBarrier中

/**
*
@hide
**/
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token
synchronized (this) {
final int token = mNextBarrierToken++;
//从消息池中获取Message
final Message msg = Message.obtain();
msg.markInUse();
//就是这里!!!初始化Message对象的时候,并没有给target赋值,因此 target==null
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
//如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步消息里有时间小于T,则prev也不为null
prev = p;
p = p.next;
}
}
//根据prev是不是为null,将 msg 按照时间顺序插入到 消息队列(链表)的合适位置
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

可以看到,这样,一条 target == null 的消息就进入了消息队列。 (也就是在消息队列中设置了同步屏障。)
接着。我们说looper会调用MessageQueue的next()来返回一个message,并且这个message队列的第一个。当MessageQueue的next()的发现队列第一个的message的target == null,那么就会遍历整个队列找出异步的消息然后排斥同步去处理。

好了全篇文章 如果好好看下来,希望 能帮忙你更理解 Handle消息机制

Handle消息机制相关推荐

  1. WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口

    原文:WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/powertoolsteam/ar ...

  2. windows程序消息机制(Winform界面更新有关)--转

    1. Windows程序消息机制 Windows GUI程序是基于消息机制的,有个主线程维护着消息泵.这个消息泵让windows程序生生不息. Windows程序有个消息队列,窗体上的所有消息是这个队 ...

  3. Android:Handler的消息机制

    前言 Android 的消息机制原理是Android进阶必学知识点之一,在Android面试也是常问问题之一.在Android中,子线程是不能直接操作View,需要切换到主线程进行.那么这个切换动作就 ...

  4. Delphi 的消息机制浅探三

    再看一段:     WM_MOUSEFIRST..WM_MOUSELAST:       if IsControlMouseMsg(TWMMouse(Message)) then       begi ...

  5. Handler消息机制(四):子线程可以创建Handler吗

    默认情况下,ActivityThread类为我们创建的了主线程的Looper和消息队列,所以当你创建Handler之后发送消息的时候,消息的轮训和handle都是在ui线程进行的.这种情况属于子线程给 ...

  6. Android的消息机制

    Android的消息机制(一) android 有一种叫消息队列的说法,这里我们可以这样理解:假如一个隧道就是一个消息队列,那么里面的每一部汽车就是一个一个消息,这里我们先忽略掉超车等种种因素,只那么 ...

  7. handler消息机制

    MessageQueue代码:http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android ...

  8. Windows消息机制要点

    1. 窗口过程     每个窗口会有一个称为窗口过程的回调函数(WndProc),它带有四个参数,分别为:窗口句柄(Window Handle),消息ID(Message ID),和两个消息参数(wP ...

  9. handler消息机制入门

    handler消息机制入门 为什么要用handle? 我们在网络上读取图片信息时,是不能把耗时操作放在主线程里面的,当我们在子线程中获取到了图片的消息的时候,我们就需要把这个数据传给主线程. 而直接使 ...

  10. 深入BCB理解VCL的消息机制

    深入BCB理解VCL的消息机制 引子:本文所谈及的技术内容都来自于Internet的公开信息.由笔者在闲暇之际整理 后,贴出来以飴网友,姑且妄称原创.每次在国外网站上找到精彩文章的时候,心中都 会暗自 ...

最新文章

  1. 新生男婴自带新冠抗体,感染者母亲如今抗体消失,医生:抗体转移了
  2. MongoDB图形化管理工具
  3. [HNOI2009]最小圈 (二分答案+负环)
  4. 二分类吸引子和鞍点的准确率的表达式ca
  5. Intent打开各种类型的文件
  6. Spring Security学习(二)
  7. 子元素相对于父元素垂直居中对齐
  8. JAVA Java多线程与并发库
  9. android图片文件的路径地址与uri的相互转换,android图片文件的路径地址与Uri的相互转换...
  10. Unity3D-协同程序
  11. 程序员的大恩人永远地离开了
  12. tensorflow 的版本差异与变化
  13. ACL-IJCNLP 2021|行业首个少样本NER数据集,清华联合阿里达摩院开发
  14. 专利号校验码php,电子专利证书的三种下载操作方法
  15. 微信小程序开发 - 模板与配置
  16. rainmeter雨滴皮肤——万花筒
  17. RINEX 3.02版本文件格式介绍
  18. 计算机科学与技术专业宣传口号,十大经典深入人心科技类广告语
  19. linux 卸载mono,Linux系统(centos7.6)安装mono3.8
  20. 电机控制系统php,基于FPGA的直流电机PWM控制系统(附带源码下载)

热门文章

  1. ORACLE-高水线的使用和直接装入数据详解
  2. 频率计的交流耦合和直流耦合的区别_直流充电桩和交流充电桩的区别
  3. python基本使用(1)
  4. kali设置中文输入法
  5. 网站新闻发布与管理系统 实训报告PPT
  6. 数据类型的大小与编译器、cpu、操作系统的关系
  7. 《项目实战》构建前后端一体化项目查询CSDN博客Top100文章质量分
  8. 基于VUE水果商城的设计与实现
  9. 对于Nacos Distro 的写请求,为什么要进行一次路由转发到责任节点
  10. 从DES走到AES(现代密码的传奇之路)