Android 线程间通信原理

此文章主涉及原理部分,需要已经懂得如何使用Handler
Handler众所周知,它是Android中切换线程的初始方案,可以大体分为: 主线程 -> 子线程 与 子线程 -> 主线程。

《先讲一个基础的 主线程->子线程原理》

使用Handler切换到子线程例子:
thread {
“开启线程,id:${Thread.currentThread().id}”.log()
Looper.prepare()//将此线程注册在Looper内的ThreadLocal中,使其唯一。
handler = Handler({ msg ->//Handler构造方法将Handler注册在Looper.myLooper中
“走Handler msg: $msg”.log()
Looper.myLooper().quitSafely()//使Looper.loop()结束,代码终于将继续向下执行
return@Handler true
})
Looper.loop()//使线程在方法内循环,等待sendMessage。PS: 真的是循环,没有sleep,没有阻塞方法,真正意义上的无延迟循环,甚至有点担心cpu占用了
“这个log最终没有打印,线程卡住了(除非looper.quitSafely)”.log()
}

上述代码创建了一个子线程的handler,在主线程随时可以借助handler切换线程。那么是如何做到切换线程的呢?秘密就在Looper.loop()中。
Looper源码一大堆,但只需要注意在Looper.loop()方法中有一个for(;;)死循环。我在Looper.loop()方法调用之后打印了日志却并没有执行也可以证明Looper.loop()方法确实导致这个线程进入了死循环
那么问题又来了,死循环和线程切换有什么关系呢?思考下,什么叫做切换线程,类似Handler那样值传递?传递一个回调让目标线程进行调用?其实归根结底就是:“让目标线程执行一个当前线程决定的操作”,而不是“当前线程去执行操作”。但是如何做到这一点呢?最简单的办法就是:“让目标线程执行死循环,反复执行一个接口,而接口的内容由当前线程实现”。

简单切换线程例子:
thread {
while (true) {//目标线程反复调用接口
runnable?.run()
runnable = null
}
}
thread {//当前线程为接口赋值
runnable = Runnable {
“啊哈”.log()
}
}

上述例子中一旦runnable被赋值,就会在目标线程执行时被调用,从而达到了切换线程执行操作的目的。而Looper.loop()其实也是这么回事。使用消息队列MessageQueue替换了简陋的runnable进行复杂的功能,但基本原理是一样的。

《那么为什么Handler必须在Looper.prepare()之后创建呢?我直接创建Handler咋就不行呢?这个prepare()有什么用呢?》

从源码中查看 prepare() 可以看到代码非常简单,仅仅是这样子:
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException(“Only one Looper may be created per thread”);
}
sThreadLocal.set(new Looper(quitAllowed));
}

若sThreadLocal有值则抛异常,无值则赋值一个新建的Looper。这个逻辑导致prepare()貌似只能执行一次,这明显不符合逻辑?其实秘密在sThreadLocal这个变量中。

《ThreadLocal 是什么?》

ThreadLocal,细节请百度,大概来说,这是一个线程局部变量,举个例子:在线程A中为threadLocal.set()赋值,在线程A中取值threadLocal.get()是有值的,但在线程B中取值threadLocal.get()是会得到null的。引用一个帖子的经典分析:
“ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。”
ThreadLocal是如何做到这件事呢?很简单,Thread中有个threadLocals集合专门用于存放线程局部变量,我们调用 ThreadLocal.set/get 其实就是对 Thread.currentThread.threadLocals 进行 set/get,所以就一定是只有当前线程的值被操作。

了解ThreadLocal后就能得知 prepare() 的作用就只是为当前线程绑定了一个 Looper 对象。
下一步是创建 Handler,Handler的构造方法中有这么一行:mLooper = Looper.myLooper(); 这也正好对应了之前的 Looper.prepare()。
最后在 loop() 中循环的任务执行代码 queue.next() 正是 Looper.myLooper().queue
到这里就理清了消息的执行机制,首先 Looper.prepare() 为线程创建唯一的 Looper 对象,其次创建的 Handler 通过ThreadLocal拿到并持有此 Looper 对象,最后 loop() 中循环执行 Looper.queue ,剩下的只需为 Looper.queue 传入消息即可。
通过 handler 提供的 send/post 系列方法即可给 Looper.queue 传入消息,至此完成线程切换。

《那么 子线程 -> 主线程是如何做到的呢?》

我这样思考:若主线程也想子线程那样死循环,一定会因为死循环而ANR,所以应该不是死循环。但实在找不到其它的切换方案。于是我开始思考主线程到底是什么?

线索1:执行方法,一个线程若想要执行一个方法,无论是Runnable接口还是普通方法,唯一的方案就是这个线程自己去调用那个方法。
所以切换到主线程执行一个方法也一定是主线程自己去执行的,而不是由其它线程执行的。
线索2:ANR,不能在主线程进行耗时操作,原因之一是会导致UI卡主一段时间,换而言之,在主线程执行的代码部分,只有(更新UI),
更新UI操作执行速度非常快,占用操作APP的时间非常短,那么主线程的其它时间实在做什么呢?wait()吗
线索3:ANR检测,主线程也只是一个线程,Java中任意一个线程卡住都不会崩溃,那么为什么继续Java的Android会ANR呢,那么答案只有:Android的源码进行了耗时检测。
如何检测一个方法的执行耗时呢?onCreate()、onClick(),SDK有几万个方法,最容易想到的检测方法是“在执行任意方法前记录开始时间,由子线程的定时器检测执行时间是否太慢,太慢就提示ANR”
再者,几万个方法必然需要封装,那如何封装呢?
根据线索判断,主动执行方法+时间充裕+封装了调用方法的部分 = 可能主线程已经在一个Looper.loop死循环中了,也只有在死循环中,才能主动执行任意方法,才能判断所有操作的执行时间。
查看 ActivityThread 源码,也可以在 main() 方法中看到一行 Looper.loop();
也是挺惊人的,从来没有想到主线程竟然是一个Looper.loop的死循环。即 子线程->主线程 是相同的原理

《Handler 系列类的职责》

Message:封装要切换线程执行的任务。
MessageQueue:任务队列,插入、拿取任务。
Looper:在各个线程中保持唯一,这是线程切换的前提,循环执行任务。
Handler:外观类,封装细节,提供便捷操作给使用者。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值