【Andorid源码解析】View.post() 到底干了啥

本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布程序员

View.post示例.png

emmm,大伙都知道,子线程是不能进行 UI 操做的,或者不少场景下,一些操做须要延迟执行,这些均可以经过 Handler 来解决。但说实话,实在是太懒了,总感受写 Handler 太麻烦了,一不当心又很容易写出内存泄漏的代码来,因此为了偷懒,我就常常用 View.post() or View.postDelay() 来代替 Handler 使用。算法

但用多了,总有点心虚,View.post() 会不会有什么隐藏的问题?因此趁有点空余时间,这段时间就来梳理一下,View.post() 原理究竟是什么,内部都作了啥事。数组

提问

开始看源码前,先提几个问题,带着问题去看源码应该会比较有效率,防止阅读源码过程当中,陷得太深,跟得太偏了。缓存

Q1: 为何 View.post() 的操做是能够对 UI 进行操做的呢,即便是在子线程中调用 View.post()?微信

Q2:网上都说 View.post() 中的操做执行时,View 的宽高已经计算完毕,因此常常看见在 Activity 的 onCreate() 里调用 View.post() 来解决获取 View 宽高为0的问题,为何能够这样作呢?数据结构

Q3:用 View.postDelay() 有可能致使内存泄漏么?app

ps:本篇分析的源码基于 andoird-25 版本,版本不同源码可能有些区别,大伙本身过源码时能够注意一下。另,下面分析过程有点长,慢慢看哈。异步

源码分析

好了,就带着这几个问题来跟着源码走吧。其实,这些问题大伙内心应该都有数了,看源码也就是为了验证内心的想法。第一个问题,之因此能够对 UI 进行操做,那内部确定也是经过 Handler 来实现了,因此看源码的时候就能够看看内部是如何对 Handler 进行封装的。而至于剩下的问题,那就在看源码过程当中顺带看看可否找到答案吧。函数

View.post()

View.post.png

View.post() 方法很简单,代码不多。那咱们就一行行的来看。oop

若是 mAttachInfo 不为空,那就调用 mAttachInfo.mHanlder.post() 方法,若是为空,则调用 getRunQueue().post() 方法。

那就找一下,mAttachInfo 是何时赋值的,能够借助 AS 的 Ctrl + F 查找功能,过滤一下 mAttachInfo = ,注意 = 号后面还有一个空格,不然你查找的时候会发现全文有两百多处匹配到。咱们只关注它是何时赋值的,使用的场景就无论了,因此过滤条件能够细一点。这样一来,全文就只有两处匹配:

dispatchAttachedToWindow.png

dispatchDetachedFromWindow.png

一处赋值,一处置空,恰好又是在对应的一个生命周期里:

  1. dispatchAttachedToWindow() 下文简称 attachedToWindow
  2. dispatchDetachedFromWindow() 下文简称 detachedFromWindow

因此,若是 mAttachInfo 不为空的时候,走的就是 Handler 的 post(),也就是 View.post() 在这种场景下,实际上就是调用的 Handler.post(),接下去就是搞清楚一点,这个 Handler 是哪里的 Handler,在哪里初始化等等,但这点能够先暂时放一边,由于 mAttachInfo 是在 attachedToWindow 时才赋值的,因此接下去关键的一点是搞懂 attachedToWindowdetachedFromWindow 这个生命周期分别在何时在哪里被调用了。

虽然咱们如今还不清楚,attachedToWindow 究竟是何时被调用的,但看到这里咱们至少清楚一点,在 Activity 的 onCreate() 期间,这个 View 的 attachedToWindow 应该是尚未被调用,也就是 mAttachInfo 这时候仍是为空,但咱们在 onCreate() 里执行 View.post() 里的操做仍然能够保证是在 View 宽高计算完毕的,也就是开头的问题 Q2,那么这点的原理显然就是在另外一个 return 那边的方法里了:getRunQueue().post()

那么,咱们就先解决 Q2 吧,为何 View.post() 能够保证操做是在 View 宽高计算完毕以后呢?跟进 getRunQueue() 看看:

getRunQueue().post()

getRunQueue.png

因此调用的实际上是 HandlerActionQueue.post() 方法,那么咱们再继续跟进去看看:

HandlerActionQueue.png

post(Runnable) 方法内部调用了 postDelayed(Runnable, long),postDelayed() 内部则是将 Runnable 和 long 做为参数建立一个 HandlerAction 对象,而后添加到 mActions 数组里。下面先看看 HandlerAction:

HandlerAction.png

很简单的数据结构,就一个 Runnable 成员变量和一个 long 成员变量。这个类做用能够理解为用于包装 View.post(Runnable) 传入的 Runnable 操做的,固然由于还有 View.postDelay() ,因此就还须要一个 long 类型的变量来保存延迟的时间了,这样一来这个数据结构就不难理解了吧。

因此,咱们调用 View.post(Runnable) 传进去的 Runnable 操做,在传到 HandlerActionQueue 里会先通过 HandlerAction 包装一下,而后再缓存起来。至于缓存的原理,HandlerActionQueue 是经过一个默认大小为4的数组保存这些 Runnable 操做的,固然,若是数组不够用时,就会经过 GrowingArrayUtils 来扩充数组,具体算法就不继续看下去了,否则愈来愈偏。

到这里,咱们先来梳理下:

当咱们在 Activity 的 onCreate() 里执行 View.post(Runnable) 时,由于这时候 View 尚未 attachedToWindow,因此这些 Runnable 操做其实并无被执行,而是先经过 HandlerActionQueue 缓存起来。

那么到何时这些 Runnable 才会被执行呢?咱们能够看看 HandlerActionQueue 这个类,它的代码很少,里面有个 executeActions() 方法,看命名就知道,这方法是用来执行这些被缓存起来的 Runnable 操做的:

executeActions.png

哇,看到重量级的人物了:Handler。看来被缓存起来没有执行的 Runnable 最后也仍是经过 Hnadler 来执行的。那么,这个 Handler 又是哪里的呢?看来关键点仍是这个方法在哪里被调用了,那就找找看:

查找调用executeActions的地方.png

借助 AS 的 Ctrl + Alt + F7 快捷键,能够查找 SDK 里的某个方法在哪些地方被调用了。

mRunQueue.executeActions.png

很好,找到了,并且只找到这个地方。其实,这个快捷键有时并无办法找到一些方法被调用的地方,这也是源码阅读过程当中使人头疼的一点,由于无法找到这些方法到底在哪些地方被调用了,因此很难把流程梳理下来。若是方法是私有的,那很好办,就用 Ctrl + F 在这个类里找一下就能够,若是匹配结果太多,那就像开头那样把过滤条件详细一点。若是方法不是私有的,那真的就很难办了,这也是一开始找到 dispatchAttachedToWindow() 后为何不继续跟踪下去转而来分析Q2:getRunQueue() 的缘由,由于用 AS 找不到 dispatchAttachedToWindow() 到底在哪些地方被谁调用了。哇,好像又扯远了,回归正题回归正题。

emmm,看来这里也绕回来了,dispatchAttachedToWindow() 看来是个关键的节点。

那到这里,咱们再次来梳理一下:

咱们使用 View.post() 时,其实内部它本身分了两种状况处理,当 View 尚未 attachedToWindow 时,经过 View.post(Runnable) 传进来的 Runnable 操做都先被缓存在 HandlerActionQueue,而后等 View 的 dispatchAttachedToWindow() 被调用时,就经过 mAttachInfo.mHandler 来执行这些被缓存起来的 Runnable 操做。从这之后到 View 被 detachedFromWindow 这段期间,若是再次调用 View.post(Runnable) 的话,那么这些 Runnable 不用再缓存了,而是直接交给 mAttachInfo.mHanlder 来执行。

以上,就是到目前咱们所能得知的信息。这样一来,Q2 是否是渐渐有一些头绪了:View.post(Runnable) 的操做之因此能够保证确定是在 View 宽高计算完毕以后才执行的,是由于这些 Runnable 操做只有在 View 的 attachedToWindowdetachedFromWiondow 这期间才会被执行。

那么,接下去就还剩两个关键点须要搞清楚了:

  1. dispatchAttachedToWindow() 是何时被调用的?
  2. mAttachInfo 是在哪里初始化的?

dispatchAttachedToWindow() & mAttachInfo

只借助 AS 的话,很难找到 dispatchAttachedToWindow() 到底在哪些地方被调用。因此,到这里,我又借助了 Source Insight 软件。
sourceInsight查找dispatchAttachedToWindow.png

很棒!找到了四个被调用的地方,三个在 ViewGroup 里,一个在 ViewRootImpl.performTraversals() 里。找到了就好,接下去继续用 AS 来分析吧,Source Insight 用不习惯,不过度析源码时确实能够结合这两个软件。

ViewRootImpl.performTraversals.png

哇,懵逼,彻底懵逼。我就想看个 View.post(),结果跟着跟着,跟到这里来了。ViewRootImpl 我在分析Android KeyEvent 点击事件分发处理流程时短暂接触过,但此次显然比上次还须要更深刻去接触,哎,力不从心啊。

我只能跟大伙确定的是,mView 是 Activity 的 DecorView。咦~,等等,这样看来 ViewRootImpl 是调用的 DecorView 的 dispatchAttachedToWindow() ,但咱们在使用 View.post() 时,这个 View 能够是任意 View,并非非得用 DecorView 吧。哈哈哈,这是否是表明着咱们找错地方了?无论了,咱们就去其余三个被调用的地方: ViewGroup 里看看吧:

ViewGroup.addViewInner.png

addViewInner() 是 ViewGroup 在添加子 View 时的内部逻辑,也就是说当 ViewGroup addView() 时,若是 mAttachInfo 不为空,就都会去调用子 View 的 dispatchAttachedToWindow(),并将本身的 mAttachInfo 传进去。还记得 View 的 dispatchAttachedToWindow() 这个方法么:

View.dispatachAttachedToWindow.png

mAttachInfo 惟一被赋值的地方也就是在这里,那么也就是说,子 View 的 mAttachInfo 其实跟父控件 ViewGroup 里的 mAttachInfo 是同一个的。那么,关键点仍是这个 mAttachInfo 何时才不为空,也就是说 ViewGroup 在 addViewInner() 时,传进去的 mAttachInfo 是在哪被赋值的呢?咱们来找找看:

查找ViewGroup的mAttachInfo.png

咦,利用 AS 的 Ctrl + 左键 怎么找不到 mAttachInfo 被定义的地方呢,无论了,那咱们用 Ctrl + F 搜索一下在 ViewGroup 类里 mAttachInfo 被赋值的地方好了:

ViewGroup里查找mAttachInfo被赋值的地方.png

咦,怎么一个地方也没有。难道说,这个 mAttachInfo 是父类 View 定义的变量么,既然 AS 找不到,咱们换 Source Insight 试试:

用SourceInsight查找mAttachInfo.png

View.mAttachInfo.png

还真的是,ViewGroup 是继承的 View,而且处于同一个包里,因此能够直接使用该变量,那这样一来,咱们岂不是又绕回来了。前面说过,dispatchAttachedToWindow() 在 ViewGroup 里有三处调用的地方,既然 addViewInner() 这里的看不出什么,那去另外两个地方看看:

ViewGroup.dispatchAttachedToWindow.png

剩下的两个地方就都是在 ViewGroup 重写的 dispatchAttachedToWindow() 方法里了,这代码也很好理解,在该方法被调用的时候,先执行 super 也就是 View 的 dispatchAttachedToWindow() 方法,还没忘记吧,mAttachInfo 就是在这里被赋值的。而后再遍历子 View,分别调用子 View 的 dispatchAttachedToWindow() 方法,并将 mAttachInfo 做为参数传递进去,这样一来,子 View 的 mAttachInfo 也都被赋值了。

但这样一来,咱们就绕进死胡同了。

咱们仍是先来梳理一下吧:

目前,咱们知道,View.post(Runnable) 的这些 Runnable 操做,在 View 被 attachedToWindow 以前会先缓存下来,而后在 dispatchAttachedToWindow() 被调用时,就将这些缓存下来的 Runnable 经过 mAttachInfo 的 mHandler 来执行。在这以后再调用 View.post(Runnable) 的话,这些 Runnable 操做就不用再被缓存了,而是直接交由 mAttachInfo 的 mHandler 来执行。

因此,咱们得搞清楚 dispatchAttachedToWindow() 在何时被调用,以及 mAttachInfo 是在哪被初始化的,由于须要知道它的变量如 mHandler 都是些什么以及验证 mHandler 执行这些 Runnable 操做是在 measure 以后的,这样才能保证此时的宽高不为0。

而后,咱们在跟踪 dispatchAttachedToWindow() 被调用的地方时,跟到了 ViewGroup 的 addViewInner() 里。在这里咱们获得的信息是若是 mAttachInfo 不为空时,会直接调用子 View 的 dispatchAttachedToWindow(),这样新 add 进来的子 View 的 mAttachInfo 就会被赋值了。但 ViewGroup 的 mAttachInfo 是父类 View 的变量,因此为不为空的关键仍是回到了 dispatchAttachedToWindow() 被调用的时机。

咱们还跟到了 ViewGroup 重写的 dispatchAttachedToWindow() 方法里,但显然,ViewGroup 重写这个方法只是为了将 attachedToWindow 这个事件通知给它全部的子 View。

因此,最后,咱们能获得的结论就是,咱们还得再回去 ViewRootImpl 里,dispatchAttachedToWindow() 被调用的地方,除了 ViewRootImpl,咱们都分析过了,得不到什么信息,只剩最后 ViewRootImpl 这里了,因此关键点确定在这里。看来此次,不行也得上了。

ViewRootImpl.performTraversals()

ViewRootImpl.performTraversals.png

这方法代码有八百多行!!不过,咱们只关注咱们须要的点就行,这样一省略无关代码来看,是否是感受代码就简单得多了。

mFirst 初始化为 true,全文只有一处赋值,因此 if(mFirst) 块里的代码只会执行一次。我对 ViewRootImpl 不是很懂,performTraversals() 这个方法应该是通知 Activity 的 View 树开始测量、布局、绘制。而 DevorView 是 Activity 视图的根布局、View 树的起点,它继承 FrameLayout,因此也是个 ViewGroup,而咱们以前对 ViewGroup 的 dispatchAttachedToWindow() 分析过了吧,在这个方法里会将 mAttachInfo 传给全部子 View。也就是说,在 Activity 首次进行 View 树的遍历绘制时,ViewRootImpl 会将本身的 mAttachInfo 经过根布局 DecorView 传递给全部的子 View 。

那么,咱们就来看看 ViewRootImpl 的 mAttachInfo 何时初始化的吧:

ViewRootImpl构造函数.png

在构造函数里对 mAttachInfo 进行初始化,传入了不少参数,咱们关注的应该是 mHandler 这个变量,因此看看这个变量定义:

mHandler.png

终于找到 new Handler() 的地方了,至于这个自定义的 Handler 类作了啥,咱们不关心,反正经过 post() 方式执行的操做跟它自定义的东西也没有多大关系。咱们关心的是在哪 new 了这个 Handler。由于每一个 Handler 在 new 的时候都会绑定一个 Looper,这里 new 的时候是无参构造函数,那默认绑定的就是当前线程的 Looper,而这句 new 代码是在主线程中执行的,因此这个 Handler 绑定的也就是主线程的 Looper。至于这些的原理,就涉及到 Handler 的源码和 ThreadLocal 的原理了,就不继续跟进了,太偏了,大伙清楚结论这点就好。

这也就是为何 View.post(Runnable) 的操做能够更新 UI 的缘由,由于这些 Runnable 操做都经过 ViewRootImpl 的 mHandler 切到主线程来执行了。

这样 Q1 就搞定了,终于搞定了一个问题,不容易啊,原本觉得很简单的来着。

跟到 ViewRootImpl 这里应该就能够停住了。至于 ViewRootImpl 跟 Activity 有什么关系、何时被实例化的、跟 DecroView 如何绑定的就不跟进了,由于我也还不是很懂,感兴趣的能够本身去看看,我在末尾会给一些参考博客。

至此,咱们清楚了 mAttachInfo 的由来,也知道了 mAttachInfo.mHandler,还知道在 Activity 首次遍历 View 树进行测量、绘制时会经过 DecorView 的 dispatchAttachedToWindow() 将 ViewRootImpl 的 mAttachInfo 传递给全部子 View,并通知全部调用 View.post(Runnable) 被缓存起来的 Runnable 操做能够执行了。

但不知道大伙会不会跟我同样还有一点疑问:看网上对 ViewRootImpl.performTraversals() 的分析:遍历 View 树进行测量、布局、绘制操做的代码显然是在调用了 dispatchAttachedToWindow() 以后才执行,那这样一来是如何保证 View.post(Runnable) 的 Runnable 操做能够获取到 View 的宽高呢?明明测量的代码 performMeasure() 是在 dispatchAttachedToWindow() 后面才执行。

performTraversals.png

我在这里卡了好久,一直没想明白。我甚至觉得是 PhoneWindow 在加载 layout 布局到 DecorView 时就进行了测量的操做,因此一直跟,跟到 LayoutInflater.inflate(),跟到了 ViewGroup.addView(),最后发现跟测量有关的操做最终都又绕回到 ViewRootImpl 中去了。

最后,感谢经过View.post()获取View的宽高引起的两个问题这篇博客的做者,解答了个人疑问。

原来是本身火候不够,对 Android 的消息机制还不大理解,这篇博客前先后后写了一两个礼拜,就是在不断查缺补漏,学习、理解相关的知识点。

大概的来说,就是咱们的 app 都是基于消息驱动机制来运行的,主线程的 Looper 会无限的循环,不断的从 MessageQueue 里取出 Message 来执行,当一个 Message 执行完后才会去取下一个 Message 来执行。而 Handler 则是用于将 Message 发送到 MessageQueue 里,等轮到 Message 执行时,又经过 Handler 发送到 Target 去执行,等执行完再取下一个 Message,如此循环下去。

清楚了这点后,咱们再回过头来看看:

performTraversals() 会先执行 dispatchAttachedToWindow(),这时候全部子 View 经过 View.post(Runnable) 缓存起来的 Runnable 操做就都会经过 mAttachInfo.mHandler 的 post() 方法将这些 Runnable 封装到 Message 里发送到 MessageQueue 里。mHandler 咱们上面也分析过了,绑定的是主线程的 Looper,因此这些 Runnable 其实都是发送到主线程的 MessageQueue 里排队,等待执行。而后 performTraversals() 继续往下工做,相继执行 performMeasure(),performLayout() 等操做。等所有执行完后,表示这个 Message 已经处理完毕,因此 Looper 才会去取下一个 Message,这时候,才有可能轮到这些 Runnable 执行。因此,这些 Runnable 操做也就确定会在 performMeasure() 操做以后才执行,宽高也就能够获取到了。画张图,帮助理解一下:

Handler消息机制.png

哇,Q2的问题终于也搞定了,也不容易啊。本篇也算是结束了。

总结

分析了半天,最后咱们来稍微小结一下:

  1. View.post(Runnable) 内部会自动分两种状况处理,当 View 还没 attachedToWindow 时,会先将这些 Runnable 操做缓存下来;不然就直接经过 mAttachInfo.mHandler 将这些 Runnable 操做 post 到主线程的 MessageQueue 中等待执行。

  2. 若是 View.post(Runnable) 的 Runnable 操做被缓存下来了,那么这些操做将会在 dispatchAttachedToWindow() 被回调时,经过 mAttachInfo.mHandler.post() 发送到主线程的 MessageQueue 中等待执行。

  3. mAttachInfo 是 ViewRootImpl 的成员变量,在构造函数中初始化,Activity View 树里全部的子 View 中的 mAttachInfo 都是 ViewRootImpl.mAttachInfo 的引用。

  4. mAttachInfo.mHandler 也是 ViewRootImpl 中的成员变量,在声明时就初始化了,因此这个 mHandler 绑定的是主线程的 Looper,因此 View.post() 的操做都会发送到主线程中执行,那么也就支持 UI 操做了。

  5. dispatchAttachedToWindow() 被调用的时机是在 ViewRootImol 的 performTraversals() 中,该方法会进行 View 树的测量、布局、绘制三大流程的操做。

  6. Handler 消息机制一般状况下是一个 Message 执行完后才去取下一个 Message 来执行(异步 Message 还没接触),因此 View.post(Runnable) 中的 Runnable 操做确定会在 performMeaure() 以后才执行,因此此时能够获取到 View 的宽高。

好了,就到这里了。至于开头所提的问题,前两个已经在上面的分析过程以及总结里都解答了。而至于剩下的问题,这里就稍微提一下:

使用 View.post(),仍是有可能会形成内存泄漏的,Handler 会形成内存泄漏的缘由是因为内部类持有外部的引用,若是任务是延迟的,就会形成外部类没法被回收。而根据咱们的分析,mAttachInfo.mHandler 只是 ViewRootImpl 一个内部类的实例,因此使用不当仍是有可能会形成内存泄漏的。

参考连接

虽然只是过一下 View.post() 的源码,但真正过下去才发现,要理解清楚,还得理解 Handler 的消息机制、ViewRootImpl 的做用、ViewRootImpl 和 Activity 的关系,什么时候绑定等等。因此,须要学的还好多,也感谢各个前辈大神费心整理的博客,下面列一些供大伙参考:

  1. scnuxisan225#经过View.post()获取View的宽高引起的两个问题

  2. kc专栏#Activity WMS ViewRootImpl三者关系

  3. 废墟的树#从ViewRootImpl类分析View绘制的流程

  4. 凶残的程序员#Android 消息机制——你真的了解Handler?


QQ图片20180316094923.jpg 最近刚开通了公众号,想激励本身坚持写做下去,初期主要分享原创的Android或Android-Tv方面的小知识,感兴趣的能够点一波关注,谢谢支持~~

相关文章
相关标签/搜索