手把手教你读懂源码,View的加载流程详细剖析
来源:互联网 发布:mysql建库指定字符集 编辑:程序博客网 时间:2024/05/22 19:25
我们都知道,在开发Android应用程序时,经常会在Activity的onCreate方法里调用setContentView方法,将布局文件或者View对象传入,但是很多人并没有去分析后续是如何加载到面并显示出来的,接下来就顺藤摸瓜将其摘下来,查看的是Android 7.1源码。
1、从setContentView 方法开始摸索
就简单从HelloWorld工程的onCreate方法开始吧:
<img src="https://pic2.zhimg.com/v2-8d8b3745d277ccc0b71daa08882ff735_b.png" class="content_image">查看其父类Activity的setContentView方法,代码如下:
<img src="https://pic1.zhimg.com/v2-9bf5fea3108845aee3e685bea5df2960_b.jpg" class="content_image">可以看出这里先得到一个 Window 对象,然后调用 Window 对象的setContentView方法。
这样分析Activity中的 setContentView 方法可以看到,界面绘制并不是由 Activity 完成的,是调用了 Window 类的 setContentView 来实现的。
所以我们继续查看 Window 类的代码:
<img src="https://pic2.zhimg.com/v2-64d1280097d5460b7813e721ad0cdd55_b.jpg" class="content_image">发现Window 类其实是一个抽象类,且 setContentView 是一个抽象方法。所以其具体实现是由 Window类的实现类来完成的(后面我们会知道该实现类是PhoneWindow)。
2、分析Window类的实现类
为了找出Window类的具体实现类,回到Activity的setContentView方法,然后进入getWindow方法:
<img src="https://pic1.zhimg.com/v2-28f40ee45f80f4c15010c890a9bcd608_b.png" class="content_image">getWindow 方法很简单,只是返回一个Window对象,那么Window对象到底是在哪儿实例化的呢?接着我们继续寻找Window对象的实例化代码,最终确认在Activity类的attach方法(attach的调用后续再分析),查看attach方法的源代码:
<img src="https://pic2.zhimg.com/v2-2bc4b9a614eb852516dc14aa47bb87ed_b.jpg" class="content_image">由此可见上面的Window对象就是PhoneWindow对象,所以我们从Activity的setContentView方法定位到了PhoneWindow的setContentView方法。
PhoneWindow的构造方法非常简单,就是获取了LayoutInflater对象,为了后续加载xml布局只有文件:
<img src="https://pic1.zhimg.com/v2-3b76894da1761fcc88f7453cd866a670_b.jpg" class="content_image">3、继续分析PhoneWindow类的setContentView方法
继续进入PhoneWindow类,查看setContentView方法源代码:
<img src="https://pic4.zhimg.com/v2-3b4f038b70d98d21a3346e790d3b9fe3_b.png" class="content_image">从源码可以知道,这里主要包括三个步骤:
如果父容器为空则初始化父容器,否则移除所有子视图;
调用LayoutInflater类的inflate方法将xml布局文件加载到父容器;
回调Callback通知ContentView发生改变,其中的Callback可能由Activity实现。
后面两步比较简单,这里主要来看第一步的父容器初始化流程,进入PhoneWindow类的installDecor方法:
<img src="https://pic3.zhimg.com/v2-9dcecf0de299c3e42d11e67753665cb6_b.jpg" class="content_image">这个方法的代码有点儿长,这里只截取重要部分,主要操作包括三部分:
调用generateDecor()创建出mDecor,即DecorView对象;
generateLayout(mDecor)传入mDecor对象,生成mContentParent ;
设置标题栏信息。
首先查看generateDecor方法,源代码如下:
<img src="https://pic4.zhimg.com/v2-c8e9a4b7fc582d645ef534552292f22b_b.png" class="content_image">这个方法非常简单,就是创建了一个DecorView对象,并返回出去。关于DecorView的具体内容可以查看其构造方法:
<img src="https://pic4.zhimg.com/v2-a95f4485aa391e981910084d17232fab_b.jpg" class="content_image">这里也是比较简单的,从这里知道了DecorView是一个FrameLayout。DecorView是Activity的顶级View,一般来说它内部包含标题栏和内容栏(加载布局文件layout.xml,即mContentParent)。内容栏是一定存在的,并且具体的id是‘content’。因此这个时候创建出的DecorView还是一个空白的FrameLayout。先不要急,这是怎么知道的,后面会继续分析。
继续回到installDecor方法中调用的generateLayout方法:
<img src="https://pic1.zhimg.com/v2-a63b704a399f5285aa5bf4d308f457b4_b.jpg" class="content_image">这个方法代码非常多,我们只需要关注重点即可。
首先获取Application android:theme=/, Activity/节点指定的themes或者代码;
然后获取窗口Features, 设置相应的修饰布局文件,这些xml文件位于frameworks/base/core/res/res/layout下;
接着调用了DecorView的onResourcesLoaded方法将上面选定的布局文件inflate为View,添加到DecorView中;
找到id为content的framlayout赋给mContentParent,由于已经将屏幕View加为mDecor的子View,因此mContentParent也是mDecor的子View;
设置mDecor的背景和标题。
这里我们先随便找一个布局文件,如screen_simple.xml:
<img src="https://pic4.zhimg.com/v2-1eeea6df3b6679d2e3626621ea0d9a77_b.png" class="content_image">就会惊奇的发现此LinearLayout就是Activity的界面,由两部分组成 ActionBar + content。从布局文件就可以认证上述所说的content,源码中id为@android:id/content的FrameLayout就是内容区域,其会赋值给PhoneWindow类中的属性mContentParent,分析后续代码后再来细看。
回到generateLayout方法,查看调用的onResourcesLoaded方法:
<img src="https://pic2.zhimg.com/v2-2e78444035d583b585d5c74068c1706d_b.jpg" class="content_image">主要就是将适配的布局文件加载进来生成root视图,调用addView方法添加到DecorView视图。
继续回到generateLayout方法,将窗口修饰布局文件中id=@android:id/content的View赋值给mContentParent, 后续自定义的view和layout都将是其子View。处理完成后就将mContentParent返回。
再回到PhoneWindow的setContentView方法中, 继续调用了mLayoutInflater.inflate(layoutResID, mContentParent),在这里就是把我们写的布局文件通过inflater加入到mContentParent中。这样我们写的布局文件成功的添加到DecorView中的mContentParent。
现在只是完成了DecorView的创建并初始化,我们还需要把这个创建并初始化完DecorView添加并显示到屏幕上,这里我们就需要用到WindowManager。
4、Activity入口
很多同学会有疑问,上面的attach方法是在哪里调用的?然后View又是如何显示出来的?我们知道,Activity的入口就是ActivityThread类,我们找到其中的handleMessage处理的代码:
<img src="https://pic4.zhimg.com/v2-18920119b1d59087cd939cbb49141a27_b.jpg" class="content_image">这里的代码非常多,但是仔细去查看,会发现很多有用的消息,会根据不同的message调用对应的方法,如handleLaunchActivity()、handleResumeActivity()、handlePauseActivity()、handleStopActivity()等,从方法名就能大概猜出来起用途。这里我们来分别查看handleLaunchActivity()和handleResumeActivity()方法,其他方法类似。
5、performLaunchActivity
首先来看handleLaunchActivity方法,源码如下:
<img src="https://pic4.zhimg.com/v2-f536ed58ca6c1c496a0bbe97eb9945cb_b.jpg" class="content_image">主要调用了performLaunchActivity方法,继续查看performLaunchActivity源代码:
<img src="https://pic3.zhimg.com/v2-f03f330a24bc23eedbaf42a11876234a_b.jpg" class="content_image">这里通过Activity的类名构建一个Activity对象,可以查看Instrumentation类的newActivity方法:
<img src="https://pic1.zhimg.com/v2-9189f460a94030f6978d13ca13189d14_b.png" class="content_image">继续回到performLaunchActivity的源代码:
<img src="https://pic3.zhimg.com/v2-2490b36bcccead1013fd80a2e7d92c82_b.jpg" class="content_image">这里是不是看到了非常熟悉的方法,就是我们前面看到的Activity类的attach()方法。就是在attach方法里面初始化PhoneWindow对象的。
后面调用了Instrumentation类的callActivityOnCreate方法,源代码如下:
<img src="https://pic4.zhimg.com/v2-9287bba1dc6b570a93930462325f0007_b.png" class="content_image">这里主要通过Instrumentation对象执行Activity的onCreate()方法,Activity的生命周期方法都是由Instrumentation对象来调用的,这里不再详细深入。
到目前为止,View只是加载到了Activity,并没有显示出来,继续研究ActivityThread的handleResumeActivity方法。
6、performResumeActivity
首先来看handleResumeActivity方法:
<img src="https://pic2.zhimg.com/v2-60fb7270c77981782cfa633dd5e14439_b.jpg" class="content_image">这里首先调用了performResumeActivity方法,查看performResumeActivity源代码:
<img src="https://pic4.zhimg.com/v2-e567fa9691ce5b0b3afdc576b53f63e3_b.jpg" class="content_image">继续调用了Activity的performResume方法,继续深入源代码:
可以看到仍然是通过Instrumentation类调用了Activity的onResume()方法。
然后回到handleResumeActivity方法,找到下面的wm.addView()方法:
<img src="https://pic1.zhimg.com/v2-2835c1cfd554025ebecd16fa16c8be14_b.jpg" class="content_image">这个方法非常关键,wm是上面a.getWindowManager()获取到的mWindowManger对象,而这个对象是WindowManagerImpl。
7、addView
继续进入WindowManagerImpl类的addView方法:
<img src="https://pic4.zhimg.com/v2-c5f74fb67220d020cea7b230fa4541cf_b.png" class="content_image">其实WindowManagerImpl类的方法大部分都是代理的WindowManagerGlobal的方法。继续进入WindowManagerGlobal类的addView方法:
<img src="https://pic2.zhimg.com/v2-1ad9ab7a360f64c961326b5e81ee6dbd_b.png" class="content_image">从上面的代码可以看出,addView方法中,创建了一个ViewRootImpl对象,然后调用ViewRootImpl.setView()方法,继续查看setView()方法。
<img src="https://pic1.zhimg.com/v2-e371040a63859ea464982ab78169319c_b.jpg" class="content_image">该方法首先将传进来的参数view赋值给mView,mView将是这个对象所认识的root节点,也是整个Activity的root的节点,即DecorView。
<img src="https://pic2.zhimg.com/v2-f1e7e4be291972fd2a08b3f4ab38c775_b.jpg" class="content_image">接着调用了requestLayout()方法,首次调度执行 layout,这里会触发 onAttachToWindow 和 创建 Surface方法。深入查看ViewRootImpl中requestLayout()方法:
<img src="https://pic4.zhimg.com/v2-a9302eebff5832e57912280cd5c16537_b.png" class="content_image">该方法首先检查了是否在主线程,然后就执行了scheduleTraversals()方法。
<img src="https://pic4.zhimg.com/v2-9a32220b4dd517d83b583edf6975334f_b.png" class="content_image">这里需要注意的就是Runnable对象,继续往后看:
这个Runnable的run()方法中,调用了doTraversal()方法:
可以看到doTraversal()方法又调用了performTraversals()方法:
<img src="https://pic4.zhimg.com/v2-e051fb778336feaf1d4a3fffcddf43eb_b.jpg" class="content_image">这个方法非常长,内部逻辑也很复杂,但是主体逻辑很清晰。其执行的过程可简单的概括为:是否需要重新计算视图的大小(measure)、是否需要重新布局视图的位置(layout),以及是否需要重绘(Draw)。也就是我们常说的View的绘制流程,由于这里涉及的内容实在太多,关于View的绘制后续再分享。
回到ViewRootImpl类的setView()方法,继续查看源码:
<img src="https://pic1.zhimg.com/v2-b3dd093400b843affe53b1194797bc0c_b.png" class="content_image">从这里可以看到view的父亲注册为自己,于是mDecor知道了自己父亲是谁,即整个Activity设置了一个根节点,在此之前调用setContentView()只是将自己的layout布局add到PhoneWindow.mContentParent,但是mDecor并不知道自己的parent是谁,现在整个view的树形结构中有了根节点,也就是ViewRootImpl,那么requestLayout()就有效了,就可以进行后面的measure、layout、draw三步操作了。
总结
那么最后再来一个精简的总结,加深理解。
用户在Activity中调用setContentView,然后调用Window的setContentView,这时会检查DecorView是否存在,如果不存在则创建DecorView对象,然后把我们自己的 View 添加到 DecorView 中。
<img src="https://pic4.zhimg.com/v2-4f4284994974c44405a6e0f374e91eb7_b.jpg" class="content_image">这里可以用一个Activity层次关系图来表示,会更加直观清晰。
<img src="https://pic2.zhimg.com/v2-1ec30caf68cbd143bd307f5bb532b37d_b.jpg" class="content_image">如果把以上这些流程梳理通透了,那么在开发中可以为我们节省不少时间了,也便于一些框架设计,也可以方便在系统的理解上实现出来多种定制任务。而且在一些中高级开发面试的时候,也会经常被问及到这方面的内容。如果还有疑问的童鞋,欢迎留言继续讨论。
今天就先分享到这里,后续将推出更多精彩内容,欢迎一起探讨学习进步。
此文章版权为微信公众号分享达人秀(ShareExpert)——鑫鱻所有,若转载请备注出处,特此声明!
https://zhuanlan.zhihu.com/p/25962829
- 手把手教你读懂源码,View的加载流程详细剖析
- 手把手教你读懂源码,View的加载流程详细剖析
- 手把手教你读懂源码,View的加载流程详细剖析
- 手把手教你读懂源码,View的绘制流程详细剖析
- 手把手教你读懂源码,View事件的注册和接收详细剖析
- 安卓View的加载流程详细剖析
- View的加载流程源码分析
- 手把手教你怎么撩妹,五分钟读懂!提取于《谈话的力量》
- hadoop-源码原理剖析--------(一)手把手教你如何进行hadoop开发的环境搭建,拒绝深坑!~
- view的加载流程
- 其实你可以读懂OKHttp3的源码
- 手把手教你写一个完整的自定义View
- 手把手教你写一个完整的自定义View
- 手把手教你写一个完整的自定义View
- Android:手把手带你深入剖析 Retrofit 2.0 源码
- Android:手把手带你深入剖析 Retrofit 2.0 源码
- 推荐!手把手教你使用Git的详细操作
- 手把手教你详细的硬件电路设计 - 全文
- 页面DOM含有iframe时产生的元素定位不准确
- python3中的urlopen对于中文url是如何处理的?
- 计数排序——C#实现
- 基本排序实现
- spring boot1.5以上版本@ConfigurationProperties取消location注解后的替代方案
- 手把手教你读懂源码,View的加载流程详细剖析
- socket编程之bind绑定失败
- 重写equals时还必须重写hashCode方法
- 安装telnet服务
- Impala视图
- 基于YARN集群构建运行PySpark Application
- 为什么说SQL正在击败NoSQL,这对数据的未来意味着什么?
- 细致分析Padding Oracle渗透测试全解析
- Dataset基于SQLAlchemy的便利工具