文章目录
一、前言
Handle
原理是一个老生常谈的事情,这里对其整个流程简单记录一下。这份理解是基于SDK31版本
二、示例代码
对于Handle
来说,主要有两个功能,一个是延迟处理消息,一个是线程间切换(参考链接:https://developer.android.google.cn/reference/android/os/Handler?hl=en)。这里简单定义个Handle
来进行说明:
Handle
有两种定义场景。一种是UI线程,一种是子线程。
UI线程
在主线程创建的话可以在子线程发送消息到主线程
class MainActivity : AppCompatActivity() {
private var handle: Handler = object : Handler(Looper.myLooper()!!){
override fun handleMessage(msg: Message) { //接受消息
super.handleMessage(msg)
Log.e("YM","-->what:${msg.what}")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
Thread{
handle.sendEmptyMessage(10) //发送消息
}.start()
}
}
子线程
以下的例子是建立一个两个子线程直接相互通信的示例
class MainActivity : AppCompatActivity() {
private var handle: Handler ?= null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
ThreadA().start()
ThreadB().start()
}
inner class ThreadA : Thread(){
override fun run() {
super.run()
Looper.prepare()
handle = object : Handler(Looper.myLooper()!!){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
Log.e("YM--->ThreadA","--->接受的参数:${msg.what}")
}
}
Log.e("YM--->ThreadA","--->循环开始")
Looper.loop()
Log.e("YM--->ThreadA","--->循环结束")
}
}
inner class ThreadB : Thread(){
override fun run() {
super.run()
Log.e("YM--->ThreadA","--->发送消息")
handle?.sendEmptyMessageDelayed(12,100L)
}
}
}
三、问题
看这篇文章的前提,是希望大家对Message
、MessageQueue
、Loop
、Handle
有个基本的概念。大致的意思如下(纯属个人理解,跟其他人理解可能不太一样)
-
Message: 一个消息实体,
Handle
进行发送消息就是发送的这种格式的消息 - MessageQueue:消息队列,用来存储消息的(说个题外话,其实消息队列没存这里面,而是采用链表方式存在Message里面,这个稍后再说)。
-
Loop:用来轮训消息的,并分发给
Handle
- Handle: 这个就是用来发消息,对消息做些简单的加工,然后将拿到的消息回传给业务代码。相当于管理器的作用。
我们先看第一个问题,就是Handle
是如何将Message
传递过去的
1、消息是如何传递的?
当我们调用Handle::sendEmptyMessage()
函数时候,该消息会进入Handle中进行发送消息,该消息在Handle
中会调用MessageQueue::enqueueMessage(Message msg, long when)
将消息添加进去。在MessageQueue::enqueueMessage(Message msg, long when)
函数中会发现,最终是调用Message.next
字段进行赋值到下一个。所以MessageQueue
类只是对消息存储的一个管理类,最终存储是在Message
中的。通过查看Message
源码可以知道,该方式是采用链表的方式进行数据存储,并不是集合的方式进行存储的。这里会延伸出几个问题,首先因为有延迟消息,也有立刻执行的消息,那么链表的排序规则是什么?
2、Message消息的排列规则是什么?
上个问题解释了在Handle
机制中是使用链表这个数据结构进行消息存储的,那么消息排列的规则是什么呢?是如何处理延迟消息和立刻发送的消息的?在上一个问题中可以知道不管哪一类消息最终都会调用MessageQueue::enqueueMessage(Message msg, long when)
进行存储消息的,该函数中有一个变量when
。其消息排列规则既是通过该时间降序排列进行判断,时间小的排在前面,时间大的排列在后面。这样就保证了消息按照顺序取出。其时间计算方式为SystemClock.uptimeMillis() + delayMillis
。默认为0。到此为止就可以知道Handle
如何将消息进行存储的。(SystemClock.uptimeMillis()
为开机到现在的时间,单位为毫秒)
3、消息是怎么发送给Handle的?
上个问题解释了消息是如何存储的及其存储规则,那么消息该如何发送给Handle
?通过官方文档或者源码说明或者网上其余的参考资料可知,是通过Loop::loop()
函数进行循环取出消息链表中的消息的。该函数最终是调用Loop.loopOnce(final Looper me,final long ident, final int thresholdOverride)
进行遍历循环的----通常来说,我们是在重写的Handle::handleMessage(msg: Message)
函数中接受到消息的,在源码中,该函数为空函数,最终由Handle::dispatchMessage(@NonNull Message msg)
函数调用。而在Loop.loopOnce(final Looper me,final long ident, final int thresholdOverride)
函数中通过Message msg = me.mQueue.next();
获取目标的消息,最终调用了msg.target.dispatchMessage(msg);
。其中Message.target
参数为Handle
。这样就将消息传递给了业务代码。
4、Loop和MessageQueue和Handle的关系是什么?
上述的几个问题解释了消息的传递流程,但是有一些地方没有解释清楚,比如Loop
是如何将Handle
和线程以及MessageQueue
绑定在一起的。通过查看源码可知Loop
类中有一个变量static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
。该变量是由静态的ThreadLocal
存储了Loop
对象。所以各个地方都可以使用同一个变量,而不会出现数据不一致的情况。由于ThreadLocal
特性的问题,可以解决每个线程对数据的访问隔离,举个例子就是A线程对ThreadLocal
进行赋值,B线程是取不出来的。这样就可以在线程中通过判断有没有值来判断是不是绑定了,这样就将Loop
和线程绑定在一起了。在通过Loop::prepare()
函数进行创建Loop
时候最终调用了私有的构造函数,如下:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
可以看到这时候创建了MessageQueue
对象,因此这时候是将Loop
和MessageQueue
进行了绑定。由于创建Handle
的时候需要将Loop
传入进去(无参构造函数被废弃了,因为有时候会出现Loop
为空的情况,所以官方建议传入Loop
),这样就完成了Handle
和Loop
的绑定。
这样可以在Handle
里面取到Loop
及其MessageQueue
5、延迟消息是什么时候发送的?
上述问题已经解释了每个Message
里面都有一个when参数,当该参数的时间不到时候,循环空转,直至时间满足条件才会进行发送消息。
6、如果创建一个新的Message也会有Handle吗?
上文说到了最终回调用Message.targe.dispatchMessage(msg)
将消息传递给Handle,那么如果new Message()
也会有Handle
吗?这个是的,因为最终会传递给Handle
。只要在里面发送消息之前赋值即可。参考代码如下:
public class Handler {
...
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this; //赋值Handle
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
...
}
7、没有消息的时候会不会停掉Loop
这个问题问法有很多种,这里解释下过程。首先轮询是需要通过Loop.loop()
进行开启,这个Loop.loop()
是一个死循环for(;;)
,所以没有数据就就会空跑起来,一直到有数据为止。除非Handle
结束了或者退出才会终止循环。Message msg = me.mQueue.next();
该代码中的next()
函数也是有一个死循环for(;;)
。这样就不会出现取不到数据返回null的情况。
8、死循环不会导致ANR吗?
ANR的情况只会出现在生命周期函数里面出现大量耗时操作才会出现ANR,但是Loop.loop()
没有在生命周期里面。还有一个需要注意的地方就是UI线程也是一个线程,如果任务结束了,那么线程就退出了,如果UI线程退出了,那么程序就会结束了,换而言之,也是Loop.loop()
是程序一直在运行。而且生命周期触发其实也是通过Handle进行触发的。在程序启动时候最终会通过ActivityThread
启动一个UI线程,其入口为main
函数,Loop
就是就是在这里进行初始化以及开始轮询的。
9、Handle是如何进行线程间切换
上面的问题已经说了会通过Handle::dispatchMessage(@NonNull Message msg)
。这个函数是定义在Handle
定义的线程里面的,通过Message进行调用,而Message是通过Loop进行轮询,Loop又是通过ThreadLocal进行保存的,由于线程隔离,所以最终会回调到其创建时候的线程里面。同时还需要理解另外两部分,一部分是存message
,一方面是取message
。存是子线程存的,取是通过绑定线程的Loop
取的。就好像是放了一份共同的数据,一个线程存,一个线程取一样。这份共同的数据就是MessageQueue
。因为Loop不知道什么时候有数据,所以在需要不停的轮询,因为轮询操作是在UI线程,所以取到数据后面执行的代码也是在UI线程。所以其实没有存在线程切换的问题,只是外在感知为切换而已。
这里面可以尝试以下简化版的线程切换代码:
class ThreadLocalTest {
@Test
fun test(){
val runnableA = ThreadA()
Thread.sleep(50)
val runnableB = ThreadB()
Thread(runnableA).start()
Thread.sleep(50)
Thread(runnableB).start()
Thread.sleep(5000)
}
private var handle: MyHandle ?= null
inner class ThreadA : Runnable{
override fun run() {
handle = object : MyHandle(){
override fun dispatchMessage(msg: String){ //用于分发消息
println("YM------>收到的消息:$msg, threadId: ${Thread.currentThread().id}")
}
}
println("YM--->ThreadA-->创建时候的线程ID:${Thread.currentThread().id}")
handle?.loop()
}
}
inner class ThreadB() : Runnable{
override fun run() {
println("YM--->ThreadB-->创建时候的线程ID:${Thread.currentThread().id}")
handle?.sendMessage("=======")
}
}
}
open class MyHandle{
private val messageList = Vector<String>() //存储消息集合,这里使用线程安全的集合,为了防止多线程出现问题,在Handle是使用synchronized处理
open fun dispatchMessage(msg: String){ //用于分发消息
}
fun sendMessage(msg: String){
messageList.add(msg)
}
//开始循环
fun loop(){
while (true){
Thread.sleep(15)//这里是为了测试,所以不让循环次数太多
for (index in 0 until messageList.size){
val value = messageList[index]
dispatchMessage(value)
messageList.remove(value)
}
}
}
}
执行结果
YM--->ThreadA-->创建时候的线程ID:24
YM--->ThreadB-->创建时候的线程ID:25
YM------>收到的消息:=======, threadId: 24
10、View::post()
有时候会通过View::post()
函数进行线程切换,其实内部也是使用Handle
进行切换的。这里可以看下官方是怎么使用Handle
。
首先View
中的Handle
函数是定义在内部静态类AttachInfo
中,该值是构造函数的必传参数。该参数在View
中的全局变量为mAttachInfo
,其赋值函数为View::dispatchAttachedToWindow(AttachInfo info, int visibility)
。该函数是通过ViewRootImpl
进行进行调用的。通过代码可知只有第一次加载进窗口的时候才会初始化。通过源码顶部的注释可知,该类是整个视图顶部的层次结构,所有View加载都会通过该类进行处理。所以当View
第一次加载进窗口的时候就已经有Handle
了。关于View先到此结束,继续Handle
的话题。View
内部的Handle
是在ViewRootImpl
中进行定义的。然后其为一个继承于Handle
的子类ViewRootHandle
。除此之外暂时没有发现有需要特别注意的地方。有一个需要注意的就是post()
添加的任务会放在绘制任务之后(该源码需要进行确认)