关于Android中为什么主线程不会因为Looper.loop()里的死循环卡死?引发的思考,事实可能不是一个 epoll 那么 简单。

( 转载请务必标明出处:https://cloud.tencent.com/developer/user/1148436/activities)

前序

本文将会把一下三个问题阐述清楚以及一个网上的普遍观点的补充:

    1,安卓 APP 启动过程,对于Activity 的 onCreate 等生命周期的函数为什么不会因为 Looper.loop()里的死循环卡死而永无机会执行。

    2,在 1 的基础上,View 的绘制到底是怎样完成的,它又为什么不会因为 Looper.loop()里的死循环卡死而永无机会刷新。

    3,网传的观点大概如下:

        1.handler机制是使用pipe来实现的

        2.主线程没有消息处理时阻塞在管道的读端

        3.binder线程会往主线程消息队列里添加消息,然后往管道写端写一个字节,这样就能唤醒主线程从管道读端返回,也就是说queue.next()会调用返回

        4.dispatchMessage()中调用onCreate, onResume

4,子线程真的不能刷新 UI ?

  其次,最终的内容我将放到两张图片上面去展示出来,源码的分析这里将不再累赘去说。第一部分网上很多,第二部分网上零散,我是通过源码分析书籍总结出来的。

  下面的阐述中,将采用:先告知答案,再放直观图片,最后文字辅助解析的顺序。

解答第一个问题

此部分的源码分析,网上很多,搜索关键字,ActivityThread,Activity 的 onCreate 在哪调用等。

总结:Activity 的 生命周期函数都是在 Looper 里面的死循环中被 ActivityThread 内部的 Handler 的 handleMessage 入口调用的,本身在循环里面调用,也就不会被阻塞,在 onCreate 等函数里面发送一个 message 也是会到这里被处理掉,仍然互不影响。

先上图,看不懂,结合后面文字看,或者源码。

文字解析,仅描述重点:

  APP 的启动过程很复杂,但是最终的入口会在 ActivityThread 类里面的 main 函数,在这个函数里面,首先会调用 Looper.prepare 目的是实例化一个 looper 对象,方便后续的主线程中的 handler 实例化获取并使用。

  因为 Handler 的消息发送和处理机制是基于 Looper 里面 MessageQueue 的,所以得先存在looper。

  然后是实例化一个自身对象,即是 new ActivityThead(),在这里面会进行内部的两个重要变量的初始化,就是后续的mAppThread Binder实例以及一个H Handler实例,当 H 发送或处理下消息的时候,使用的Looper就是上面实例化的。然后是它自身的 attach(...)函数,在内部进行 AMS(ActivityManagerSevice)和mAppThread Binder进程通讯者的绑定,即是AMS的attachApplication(...)的调用。

  随后调用AMS的attachApplicationLocked(...),在这个函数里面,将会进行第一次跨进程通讯,AMS运行在系统进程,而我们的APP是另外一个进程。此时在AMS里面会调用我们上面绑定的mAppThread binder对象的bindApplication(...)方法,触发BIND_APPLICATION消息,该消息由 H 来进行发送,也就是 sendMessage(...),此时消息会在 Looper 里面的 loop() 进行处理。

  像 Handler 源码一样,最后会在 H.handleMessage(...) 处理, 然后就是进入到对应的函数里面进行 Context 的初始化,Application开始初始化,并且调用它的 onCreate,等其他操作。

  上面为第一部分,接下来是第二部分。在AMS的attachApplicationLocked(...)函数里面,在触发了第一次进程通讯后,代码接着运行,会在里面进行第二次的进程通讯,首先是Activity的栈管理者之一ActivityStackSupervisor调用它的attachApplicationLocked(...),在里面调用 realStartActivityLocked(...),然后是正式发起第二次IPC,触发 LAUNCH_ACTIVITY 消息,一样是 H 发送和处理,处理处调用 performLaunchActivity(...),在这里面,根据一直传下来的信息,将会 new 一个 Actiivity,然后就是调用它的 onCreate,onStart。

第二个问题

声明:此部分的内部十分地复杂!包括下面的图与文字解析在内,仅作抛砖引玉,是个人总结的大概流程。关于源码分析,网上很零散,十分建议看源码分析类书籍。

总结:View 的底层绘制是基于Binder进程通讯触发,由底层 SurfaceFlinger 的工作线程中的事件机制,包含 handler ,looper,messageQueue 来进行接收、处理,它和 ActivityThread 的 Handler 没关系,即是与 ActivityThread 几乎无关,但是如果在ActivityThread 里面调用 View的相关函数,例如 handleMessage 的一个 setText(..),最终触发到View其它底层函数,它将会将这些信息发送到 SurfaceFlinger 的事件机制中去,被对应处理,最终刷新到界面。

文字解析,里面所有函数和变量都是底层C++代码 的。Android 的GUI系统,也是图形界面系统,其依赖于OpenGL,EGL 等函数库,同时Android硬件HAL层的接口Composer的直接使用者是SurfaceFlinger,SurfaceFlinger,对于OpenGL而言,是一个很重要 ”应用“,它依赖于OpenGL,EGL的函数库,并使用他们的API来进行图形的最终绘制。

  SurfaceFlinger 在启动时会先进行自己内部的一个工作线程实例化和运行,该线程在后面承担着整个的绘制事件流程,在运行该线程时,会先进行MessageQueue内部的 looper 和 handler 的实例化,然后再 Run,Run 内部启动了事件的循环。

  从这一刻开始,它将进入到 waitForEvent(...)方法,这里是个死循环,并在里面调用 waitMessage(...),waitMessage 里面将会调用 looper的pollOnce(...),该方法和 ActivityThread 的 loope() 内部的 next() 里面的 queue.next() 差不多,不同的是 pollOnce(...) 会调用 MessageQueue 内部的函数 eventReceiver(...) 。

  eventReceiver 内部将会对进程中的消息获取,如果有收到其它进程传过来的对应的VSync 消息,那么将会对其进行下一步的分发,就是 dispatchInvalidate(...) 或 dispatchRefesh(...),最后会进入到 handler 的 handlMessage,然后回调 SurfaceFlinger 的 onMessgeReceiver(...),内部再调用 handleMessageRefresh(...)。

  然后是 SurfaceFlinger 的 layer层对View改变的绘制,绘制结合 NativeWindow 和 FrameBuffer 的缓存技术,最终将结果呈现到终端。代码非常复杂。

第三个补充

  网上对于博文标题的这个问题的解析普遍是:见前序的第三点,这里要补充的是,如果是 View 的 UI 刷新,不会导致阻塞的原因是本文的第二个解释,View 的绘制与 Java 代码的looper无关,而是由底层 SurfaceFlinger 自身的事件处理机制处理的。对于第一个问题的解析,那么可以参考前序第三点的内容。

第四个问题

   如果您有耐心看到这里,非常感谢,可能有朋友会想起 Android 的另外一句名言,子线程不能刷新UI,这样是否和上面说的冲突呢?其实不然,看下下面的代码片段,它是源码里面限制我们在子线程刷新UI的。

 1 void checkThread() {
 2 
 3         if (mThread != Thread.currentThread()) {
 4 
 5             throw new CalledFromWrongThreadException(
 6 
 7                     "Only the original thread that created a view hierarchy can touch its views.");
 8 
 9         }
10 
11 }

代码第三行,其中 mThread 是创建 ViewRootImpl 的线程,而ViewRootImpl是在主线程中创建的,所以,我们习惯地称它为主线程,mThread和当前代码运行的线程来做了个等式运算,相同就出错,也就是说,并不是子线程不能刷新UI,准确来说,是发送进行 UI 刷新消息的消息,因为真正的底层刷新也不是当前 APP 的主线程。而是限制了,如果当ViewRootImpl是由子线程创造的,那么就可以在该子线程中发送更新UI的消息,自然地就能更新了,那么为什么限制呢?

  下面解析引自知乎

  因为不光是gui,同样的道理在几乎所有编程领域里都是这样的,这背后是线程同步的开销问题。显然两个线程不能同时draw,否则屏幕会花;不能同时insert map,否则内存会花;不能同时write buffer,否则文件会花。需要互斥,比如锁。结果就是同一时刻只有一个线程可以做ui。那么当两个线程互斥几率较大时,或者保证互斥的代码复杂时,选择其中一个长期持有其他发消息就是典型的解决方案。所以普遍的要求ui只能单线程。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏后端之路

redis无法获取连接Could not get a resource from the pool分析

背景 随着系统使用用户上升,我们也愈发多的使用到了redis组件! 比如在做session共享时 tomcat使用redis做session 参考https:/...

59210
来自专栏BeJavaGod

省市区 - 三级联动通用化模块组件

都说我们要做模块化设计,而不要做功能化设计 什么是模块化设计,就是可插拔性高,组件化,想要就用,不要用拉倒,直接删除就行 什么是功能化设计,就是一个简单的功能,...

4415
来自专栏coolblog.xyz技术专栏

Spring IOC 容器源码分析系列文章导读

Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已...

28410
来自专栏前端杂货铺

Blob初探

简介   Blob在js中意味着二进制大数据。实现该接口的对象有3个属性,分别是type(MIME),size(byte)和 一个切割方法:slice(在大文件...

3143
来自专栏Golang语言社区

Go语言开发RESTFul JSON API

也许我们之前有使用过各种各样的API, 当我们遇到设计很糟糕的API的时候,简直感觉崩溃至极。希望通过本文之后,能对设计良好的RESTful API有一个初步认...

8503
来自专栏小巫技术博客

Android Crash之Java Crash分析

842
来自专栏张戈的专栏

移动搜索SEO:网站移动适配之Meta标注、移动跳转终结篇

这些天,在给博客的标签页(tag)添加跳转和 META 动态申明时,居然让我醍醐灌顶,发现之前的动态适配的做法是多么的苦逼和小白! 总结前,先来回顾下小白张戈在...

4906
来自专栏一个番茄说

Swift中防止ptrace依附

在移动开发中,安全是一个很重要的话题,当然安全是没有绝对的,只能说尽可能的提高安全性。在iOS的开发中,为了防止别人窥视我们的App,我们得采用一些手段来进行防...

1083
来自专栏小灰灰

Java & PhantomJs 实现html输出图片

Java & PhantomJs 实现html输出图片 借助phantomJs来实现将html网页输出为图片 I. 背景 如何在小程序里面生成一张图,分享到朋...

6448
来自专栏Seebug漏洞平台

CVE-2017-16943 Exim UAF漏洞分析

感恩节那天,meh在Bugzilla上提交了一个exim的uaf漏洞:https://bugs.exim.org/show_bug.cgi?id=2199,这周...

4145

扫码关注云+社区