前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于Android中为什么主线程不会因为Looper.loop()里的死循环卡死?引发的思考,事实可能不是一个 epoll 那么 简单。

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

作者头像
林冠宏-指尖下的幽灵
发布2018-01-03 11:17:26
1.3K0
发布2018-01-03 11:17:26
举报

( 转载请务必标明出处: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的。

代码语言:javascript
复制
 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只能单线程。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-02-18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档