Android 线程间通信的原理

此文章主涉及原理部分,需要已经懂得如何使用Handler

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.send来切换线程。那么是如何做到线程通信的呢?秘密就在Looper.loop()中。

Looper.loop()源码一大堆,但只需要注意到在Looper.loop()方法中有一个for(;;)死循环。我在Looper.loop()方法调用之后打印了日志,却并没有执行也可以证明Looper.loop()方法确实导致这个线程进入了死循环

那么问题又来了,死循环和线程切换有什么关系呢?试想下,线程通信指的是什么?值传递?传递一个回调让目标线程进行调用?归根结底就是:“让目标线程执行一个当前线程决定的操作”并不是当前线程去执行操作,而是目标线程去执行操作。但是如何做到这一点呢?最简单的办法就是:“让目标线程执行死循环,反复执行一个接口,而接口的内容由当前线程实现”,如这样:

thread {
    while (true) {//目标线程反复调用接口
        runnable?.run()
        runnable = null
    }
}
thread {//当前线程为接口赋值
    runnable = Runnable {
        "啊哈".log()
    }
}

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

那么基本原理是搞懂了,MessageQueue也不讲述,Looper.prepar()和必须在其之后执行创建的Handler又是怎么回事呢?为什么仅仅一个handler.send就可以实现线程切换了呢?

众所周知,在第一块代码中创建的handler可以在任意线程中send而在子线程中执行。那么这个handler一定在Looper.loop()中有引用,但这个引用是怎么指向的,以及是何时赋值的,就得讲Looper.prepare()了,点进Looper.prepare()可以看到源码非常简单。

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()只能执行一次,那岂不完蛋了?只能在一个子线程中使用Looper.prepare()么?其实秘密在sThreadLocal这个变量中,

ThreadLocal。见名思意,这是一个线程局部变量,举个例子:在线程A中为threadLocal.set()赋值,在线程A中取值threadLocal.get()是有值的,但在线程B中取值threadLocal.get()是会得到null的。引用一个帖子的经典分析:“ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。”

那么了解ThreadLocal后就知道了在Looper的静态方法中,一个线程将对应一个Looper,在之,查看Handler的构造方法源码,发现了对looper的赋值,即Looper与handler的关联是looper.queue,也就是MessageQueue。知道这两点后在关联loop()中的死循环,便懂得了消息传递的步骤:消息 -> handler -> looper.queue。loop()死循环 -> looper.queue 至此懂得了 主线程 -> 子线程的原理。

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

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

线索1:执行方法,一个线程若想要执行一个方法,无论是线程方法还是普通方法,唯一的执行步骤就是这个线程自己去调用那个方法。
            所以主线程执行一个方法也一定是主线程自己去执行的,不是其它线程说“主线程,你去执行这个”,而是主线程说“我要执行这个”
线索2:ANR,不能在主线程进行耗时操作,原因之一是会导致UI卡主一段时间,换而言之,在主线程执行的代码部分,只有(更新UI),
            其执行速度非常之快,占用操作APP的时间非常之短,那么主线程的其它时间实在做什么呢?wait()吗
线索3:ANR检测,主线程也只是一个线程,Java中主线程卡主并不会崩溃,那么为什么Android会ANR呢,那么答案只有:Android的源码进行了耗时检测。
            那么如何检测一个方法的执行耗时呢?onCreate()、onClick(),成吨的方法,最容易想到的检测方法是“在执行某个方法前更新执行时间,开启定时器检测时间更新频率,若执行时间距当前时间太远则视为ANR”
            再者,成吨的方法必然需要封装,否则一个一个写非常愚蠢,那如何封装呢?
根据线索判断,主动执行方法+时间充裕+封装了调用方法的部分 = 可能主线程已经在一个Looper.loop死循环中了,也只有在死循环中,才能主动执行任意方法,才能判断所有操作的执行时间。
也是挺惊人的,从来没有想到主线程竟然是一个Looper.loop的死循环。即 子线程->主线程 是相同的原理

猜你喜欢

转载自blog.csdn.net/qq_34224268/article/details/79608466
今日推荐