【移动应用开发技术】源码详解Android中View.post()用法_第1页
【移动应用开发技术】源码详解Android中View.post()用法_第2页
【移动应用开发技术】源码详解Android中View.post()用法_第3页
【移动应用开发技术】源码详解Android中View.post()用法_第4页
【移动应用开发技术】源码详解Android中View.post()用法_第5页
已阅读5页,还剩9页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

【移动应用开发技术】源码详解Android中View.post()用法

emmm,大伙都知道,子线程是不能进行UI操作的,或者很多场景下,一些操作需要延迟执行,这些都可以通过Handler来解决。但说实话,实在是太懒了,总感觉写Handler太麻烦了,一不小心又很容易写出内存泄漏的代码来,所以为了偷懒,我就经常用View.post()orView.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()有可能导致内存泄漏么?ps:本篇分析的源码基于andoird-25版本,版本不一样源码可能有些区别,大伙自己过源码时可以注意一下。另,下面分析过程有点长,慢慢看哈。源码分析好了,就带着这几个问题来跟着源码走吧。其实,这些问题大伙心里应该都有数了,看源码也就是为了验证心里的想法。第一个问题,之所以可以对UI进行操作,那内部肯定也是通过Handler来实现了,所以看源码的时候就可以看看内部是如何对Handler进行封装的。而至于剩下的问题,那就在看源码过程中顺带看看能否找到答案吧。View.post()View.post()方法很简单,代码很少。那我们就一行行的来看。如果mAttachInfo不为空,那就调用mAttachInfo.mHanlder.post()方法,如果为空,则调用getRunQueue().post()方法。那就找一下,mAttachInfo是什么时候赋值的,可以借助AS的Ctrl+F查找功能,过滤一下mAttachInfo=,注意=号后面还有一个空格,否则你查找的时候会发现全文有两百多处匹配到。我们只关注它是什么时候赋值的,使用的场景就不管了,所以过滤条件可以细一点。这样一来,全文就只有两处匹配:一处赋值,一处置空,刚好又是在对应的一个生命周期里:dispatchAttachedToWindow()下文简称attachedToWindow

dispatchDetachedFromWindow()下文简称detachedFromWindow。所以,如果mAttachInfo不为空的时候,走的就是Handler的post(),也就是View.post()在这种场景下,实际上就是调用的Handler.post(),接下去就是搞清楚一点,这个Handler是哪里的Handler,在哪里初始化等等,但这点可以先暂时放一边,因为mAttachInfo是在attachedToWindow时才赋值的,所以接下去关键的一点是搞懂attachedToWindow到detachedFromWindow这个生命周期分别在什么时候在哪里被调用了。虽然我们现在还不清楚,attachedToWindow到底是什么时候被调用的,但看到这里我们至少清楚一点,在Activity的onCreate()期间,这个View的attachedToWindow应该是还没有被调用,也就是mAttachInfo这时候还是为空,但我们在onCreate()里执行View.post()里的操作仍然可以保证是在View宽高计算完毕的,也就是开头的问题Q2,那么这点的原理显然就是在另一个return那边的方法里了:getRunQueue().post()。那么,我们就先解决Q2吧,为什么View.post()可以保证操作是在View宽高计算完毕之后呢?跟进getRunQueue()看看:getRunQueue().post()所以调用的其实是HandlerActionQueue.post()方法,那么我们再继续跟进去看看:post(Runnable)方法内部调用了postDelayed(Runnable,long),postDelayed()内部则是将Runnable和long作为参数创建一个HandlerAction对象,然后添加到mActions数组里。下面先看看HandlerAction:很简单的数据结构,就一个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操作的:哇,看到重量级的人物了:Handler。看来被缓存起来没有执行的Runnable最后也还是通过Hnadler来执行的。那么,这个Handler又是哪里的呢?看来关键点还是这个方法在哪里被调用了,那就找找看:借助AS的Ctrl+Alt+F7快捷键,可以查找SDK里的某个方法在哪些地方被调用了。很好,找到了,而且只找到这个地方。其实,这个快捷键有时并没有办法找到一些方法被调用的地方,这也是源码阅读过程中令人头疼的一点,因为没法找到这些方法到底在哪些地方被调用了,所以很难把流程梳理下来。如果方法是私有的,那很好办,就用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的attachedToWindow到detachedFromWiondow这期间才会被执行。那么,接下去就还剩两个关键点需要搞清楚了:dispatchAttachedToWindow()是什么时候被调用的?mAttachInfo是在哪里初始化的?dispatchAttachedToWindow()&mAttachInfo只借助AS的话,很难找到dispatchAttachedToWindow()到底在哪些地方被调用。所以,到这里,我又借助了SourceInsight软件。

很棒!找到了四个被调用的地方,三个在ViewGroup里,一个在ViewRootImpl.performTraversals()里。找到了就好,接下去继续用AS来分析吧,SourceInsight用不习惯,不过分析源码时确实可以结合这两个软件。哇,懵逼,完全懵逼。我就想看个View.post(),结果跟着跟着,跟到这里来了。ViewRootImpl我在分析AndroidKeyEvent点击事件分发处理流程时短暂接触过,但这次显然比上次还需要更深入去接触,哎,力不从心啊。我只能跟大伙肯定的是,mView是Activity的DecorView。咦~,等等,这样看来ViewRootImpl是调用的DecorView的dispatchAttachedToWindow(),但我们在使用View.post()时,这个View可以是任意View,并不是非得用DecorView吧。哈哈哈,这是不是代表着我们找错地方了?不管了,我们就去其他三个被调用的地方:ViewGroup里看看吧:addViewInner()是ViewGroup在添加子View时的内部逻辑,也就是说当ViewGroupaddView()时,如果mAttachInfo不为空,就都会去调用子View的dispatchAttachedToWindow(),并将自己的mAttachInfo传进去。还记得View的dispatchAttachedToWindow()这个方法么:mAttachInfo唯一被赋值的地方也就是在这里,那么也就是说,子View的mAttachInfo其实跟父控件ViewGroup里的mAttachInfo是同一个的。那么,关键点还是这个mAttachInfo什么时候才不为空,也就是说ViewGroup在addViewInner()时,传进去的mAttachInfo是在哪被赋值的呢?我们来找找看:咦,利用AS的Ctrl+左键怎么找不到mAttachInfo被定义的地方呢,不管了,那我们用Ctrl+F搜索一下在ViewGroup类里mAttachInfo被赋值的地方好了:咦,怎么一个地方也没有。难道说,这个mAttachInfo是父类View定义的变量么,既然AS找不到,我们换SourceInsight试试:还真的是,ViewGroup是继承的View,并且处于同一个包里,所以可以直接使用该变量,那这样一来,我们岂不是又绕回来了。前面说过,dispatchAttachedToWindow()在ViewGroup里有三处调用的地方,既然addViewInner()这里的看不出什么,那去另外两个地方看看:剩下的两个地方就都是在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()这方法代码有八百多行!!不过,我们只关注我们需要的点就行,这样一省略无关代码来看,是不是感觉代码就简单得多了。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什么时候初始化的吧:在构造函数里对mAttachInfo进行初始化,传入了很多参数,我们关注的应该是mHandler这个变量,所以看看这个变量定义:终于找到newHandler()的地方了,至于这个自定义的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()后面才执行。我在这里卡了很久,一直没想明白。我甚至以为是PhoneWindow在加载layout布局到DecorView时就进行了测量的操作,所以一直跟,跟到LayoutInflater.inflate(),跟到了ViewGroup.addView(),最后发现跟测量有关的操作最终都又绕回到ViewRootImpl中去了。原来是自己火候不够,对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()操作之后才执行,宽高也就可以获取到了。画张图,帮助理解一下:哇,Q2的问题终于也搞定了,也不容易啊。本篇也算是结束了。总结分析了半天,最后我们来稍微小结一下:View.post(R

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论