从XML到View显示在屏幕上,都发生了什么
前言
View
绘制可以说是Android
开发的必备技能,但是关于View
绘制的的知识点也有些繁杂。
如果我们从头开始阅读源码,往往千头万绪,抓不住要领。
目前当我们写页面时,布局都是写在XML
里的,我们可以思考下:布局从XML
到显示到屏幕上,都发生了什么,可以分为哪几个部分?
我们将整个显示流程分解为以下几个部分
- 代码是怎么从
XML
转换成View
的? View
是怎么添加到页面上的?- 在内存中
View
到底是怎么绘制的? View
绘制完成后是怎么显示到屏幕上的?
本文目录如下所示:
1. XML
是怎么转换成View
的?
我们都知道,在android
中写布局一般是通过XML
,然后通过setContentView
方法配置到页面中
看来XML
转换成View
就是在这个setContentView
中了
1.1 setContentView
中做了什么
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
可以看到resId
传给了我们熟悉的LayoutInflater
,看来xml
转化成View
就是在LayoutInflater
方法中实现的了
1.2 LayoutInflater
中做了什么?
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
//预编译直接返回view,目前还未启用
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
//真正将`XML`转化为`View`
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
代码也比较简单,我们一起来分析下
- 首先我们需要明确,将
XML
转化为View
牵涉到一些耗时操作,比如XML
解析是一个io
操作,将XML
转化为View
涉及到反射,这也是耗时的 - 我们可以看到在解析前有个
tryInflatePrecompiled
方法,这个方法就是希望可以在编译阶段直接预编译XML
,在运行时直接返回构建好的View
,看起来Google
希望通过这种方式解决XML
的性能问题。不过这个功能目前还没有启用,因此此方法直接返回null
,目前生效的还是下面的方法 - 真正将
XML
解析为View
的还是在inflate
方法中,将标签名转化为View
的名称,XML
中的各种属性转化为AttributeSet
对象,然后通过反射生成View
对象
由于篇幅原因,这里就不再粘贴inflate
方法的源码了,里面主要需要注意下setFactory
与setFactory2
方法
在真正进行反射前,会先调用这两个方法尝试创建一下View
,而且系统开放了API
,我们可以自定义解析XML
方式
这就给了我们一些面向切面编程的空间,可以利用这两个API
实现换肤,替换字体,替换 View
,提升View
构建速度等操作
希望进一步了解的同学可参考:探究 LayoutInflater setFactory
1.3 小结
XML
转化为View
转化为主要是通过LayoutInflator
来完成的,将标签名转化为View
的名称,XML
中的各种属性转化为AttributeSet
对象,然后通过反射生成View
对象
这个过程中存在一些耗时操作,比如解析XML
的IO
操作,通过反射生成View
等,我们可以通过多种方式优化这个过程,比如将反向的耗时转移到编译期,有兴趣的同学可以参阅:Android "退一步"的布局加载优化
2. View
是怎么添加到页面上的?
经过上面这步,View
已经被创建出来了,但是View
又是怎么添加到页面(Activity
)上的呢?
我们再来看下setContentView
方法
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
LayoutInflater
有两个参数,第二个参数就是root
,即创建出的view
要被添加的父view
所以答案也就呼之欲出了,创建出来的view
被添加到了contentParent
上,即R.id.content
上
那么问题来了,这个R.id.content
是哪来的呢?
2.1 R.id.content
从何而来?
我们看到,setContentView
开头调用了ensureSubDecor
方法,一起来看下它的源码
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
}
private ViewGroup createSubDecor() {
// Now let's make sure that the Window has installed its decor by retrieving it
ensureWindow();
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
//省略其他样式subDecor布局的实例化
//包含 actionBar floatTitle ActionMode等样式
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
// 把`contentView`的id设置为android.R.id.content,把windowContentView的id设置为View.NO_ID
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
//将subDecor添加到window
mWindow.setContentView(subDecor);
return subDecor;
}
可以看出,主要工作是创建subDecor
并添加到window
上
- 步骤一:确认
window
并attach
(设置背景等操作) - 步骤二:获取
DecorView
,因为是第一次调用所以会installDecor
(创建DecorView
和windowContentView
) - 步骤三:从
xml
中实例化出subDecor
布局 - 步骤四:将
subDecor
的contentView
的id
设置为R.id.content
- 步骤四:将
subDecor
添加到window
中
现在我们已经知道R.id.content
从何而来了,并且知道了subDecor
最终会添加到window
中
那么问题来了,window
又是什么呢?
2.2 window
到底是什么?
我们上文提到,我们创建的view
会被添加到subDecor
上,最后会被添加到window
中,那么window
是什么?为什么要有window
?
我们在应用中有多个页面,手机上也有多个应用,这么多页面同时只能有一个页面显示在手机上,这个时候就需要有一个机制来管理当前显示哪个页面
于是Android
在系统进程中创建了一个系统服务WindowManagerService(WMS)
专门用来管理屏幕上的窗口
,而View
只能显示在对应的窗口
上,如果不符合规定就不开辟窗口
进而对应的View
也无法显示
window
机制就是为了管理屏幕上的view
的显示以及触摸事件的传递问题
值得注意的事,上面的window
与窗口
很容易混淆,Android SDK
中的Window
是一个抽象类,它有一个唯一实现类PhoneWindow
,PhoneWindow
内部会持有一个DecorView
(根View
),它的职责就是对DecorView
做一些标准化的处理,比如标题、背景、导航栏、事件中转等,很显然与我们前面所说的窗口概念不符合
总得来说PhoneWindow
只是提供些标准的UI
方案,与窗口
不等价窗口
是一个抽象概念,即当前应该显示哪个页面,系统通过WindowManagerService(WMS)
来管理
关于窗口
机制,想了解更加详细的同学,可参考:通俗易懂 Android视图系统的设计与实现,写得非常通俗易懂,有兴趣的可以了解下
2.3 View
什么时候真正可见?
上面提到PhoneWindow
只是提供些标准的UI
方案,并不是真正的窗口
那么我们的View
到底什么时候添加到窗口上,什么时候真正对用户可见?
#ActivityThread
public void handleResumeActivity(...) {
//...
//注释1
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
...
//注释2
wm.addView(decor, l);
...
}
#ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//记录DecorView
mView = view;
//省略
//开启View的三大流程(measure、layout、draw)
requestLayout();
try {
//添加到WindowManagerService里,这里是真正添加window到底层
//这里的返回值判断window是否成功添加,权限判断等。
//比如用Application的context开启dialog,这里会添加不成功
// 注释3
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
} catch (RemoteException e) {
}
//省略
//输入事件接收
}
}
}
- 注释1处会从
Activity
中取出PhoneWindow
,DecorView
,WindowManager
- 注释2处调用了
WindowManager
的addView
方法,顾名思义就是将DecorView
添加至窗口
当中 - 最后会调到
ViewRootImpl
中注释3处,这里才是真正的通过WMS
在屏幕上开辟一个窗口,到这一步我们的View
也就可以显示到屏幕上了
可以看出,当我们打开一个Activity
时,界面真正可见是在onResume
之后
2.4 Activity
,PhoneWindow
,View
的关系
Phonewindow
是activity
的一个成员变量,会在Activity.attatch
时初始化PhoneWindow
是View
的容器,对DecorView
做一些标准化的处理,比如标题、背景、导航栏、事件中转等Activity
则提供了窗口
的生命周期,屏蔽了窗口机制的复杂细节,开发者只需要基于模板方法开发即可
如下图所示
2.5 小结
View
添加到页面上,主要经过了这么几个过程
- 1.启动
activity
- 2.创建
PhoneWindow
- 3.设置布局
setContentView
,将layoutId
转化为View
- 4.确认
subDecorView
的初始化,将subDecorView
添加到PhoneWindow
中 - 5.添加
layoutId
转化后的View
到android.R.id.content
上 - 6.在
onResume
中将DecorViewView
添加到WindowManager
中 - 7.
View
真正显示到屏幕上了
3. View
到底是怎么绘制的?
经过上一步,View
已经添加到window
上了,接下来就是View
本身的绘制了View
的绘制主要经过以下几步
1、首先需要确定View
占的空间尺寸(measure
)
2、确定了空间尺寸,就需要确定摆放在哪个位置(layout
)
3、确认了摆放位置,就需要确定在上面展示些什么东西(draw
)
这几个阶段,View
已经封装了模板方法给我们,我们直接重写onMeasure
,onLayout
,onDraw
这几个方法就好了
而绘制的入口,就是上面ViewRootImpl.setView
中的requestLayout
3.1 requestLayout
如何触发绘制
上文说到requestLayout
会触发绘制,我们一起来看下源码
ViewRootImpl.java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查是否是主线程,如果不是则直接抛出异常,ViewRootImpl创建的时候生成一个主线程引用
//用当前线程和引用比较,如果是同一个则是主线程
//这也是为什么在子线程对View进行更新、绘制会报错的原因
checkThread();
//用来标记需要进行layout
mLayoutRequested = true;
//绘制请求
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
//标记一次绘制请求,用来屏蔽短时间内的重复请求
mTraversalScheduled = true;
//往主线程Looper队列里放同步屏障消息,用来控制异步消息的执行
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//放入mChoreographer队列里
//主要是将mTraversalRunnable放入队列
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//省略
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
//没有取消绘制的话则开始绘制
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
//真正开始执行measure、layout、draw等方法
performTraversals();
}
}
requestLayout
中其实主要也是做了以下几件事
- 检查绘制的线程与
View
创建的线程是否是同一个线程 - 通过
Handler
同步屏障机制,保证UI
绘制消息优先级是最高的 - 将
mTraversalRunnable
传入Choreographer
,监听vsync
信号。 - 收到
vsync
信号后会回调TraversalRunnable
,移除同步屏障并开始真正的measure
,layout
,draw
View
绘制流程图如下:
3.2 MeasureSpec
分析
在测量过程中,会传入一个MeasureSpec
参数,MeasureSpec
封装了View
的规格尺寸参数,包括View
的宽高以及测量模式。
它的高2位代表测量模式,低30位代表尺寸。其中测量模式总共有3中。
UNSPECIFIED
:未指定模式不对子View
的尺寸进行限制。AT_MOST
:最大模式对应于wrap_content
属性,父容器已经确定子View
的大小,并且子View
不能大于这个值。EXACTLY
:精确模式对应于match_parent
属性和具体的数值,子View
可以达到父容器指定大小的值。
普通view
的MeasureSpec
创建规则如下
结合这个表,我们可以一起来看一个问题
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/red"
xmlns:android="http://schemas.android.com/apk/res/android">
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/blue"/>
</FrameLayout>
请问这样一个布局,最后是什么颜色呢?
答案是蓝色,并且占满屏幕
简单来说,当我们自定义View
时,如果没有对MODE
做处理,设置wrap_content
和match_content
结果其实是一样的,View
的宽高都是取父 View
的宽高
本问题的详细解析可见:一道滴滴面试题
3.3 小结
View
的绘制需要定位,测量,绘制三个步骤,为了简化自定义View
的过程,官方已经提供了模板方法,我们重写相关方法即可ViewRootImpl
中的requestLayout
是绘制的入口,当然我们在View
中调用invalidate
或者requestLayout
也会触发重绘- 绘制过程本质上也是通过
Handler
发送消息,为了提高绘制消息的优先级,会开启同步屏蔽机制 - 将
mTraversalRunnable
传入Choreographer
,监听vsync
信号。注意,vsync
信号注册了才会监听。 - 收到
vsync
信号后会回调TraversalRunnable
,移除同步屏障并开始真正的measure
,layout
,draw
过程 - 接下来就是回调各个
View
的onMeasure
,onLayout
,onDraw
过程
4 View
绘制完成后是怎么显示到屏幕上的?
目前我们已经知道了,从XML
到调用View.onDraw
的过程,但是从onDraw
到显示到屏幕上似乎还有些距离
我们知道,View
最后要显示在屏幕上,CPU
负责计算帧数据,把计算好的数据交给GPU
,GPU
会对图形数据进行渲染,渲染好后放到buffer
(图像缓冲区)里存起来,然后Display
(屏幕或显示器)负责把buffer
里的数据呈现到屏幕上
那么问题来了,canvas.draw
是怎么转化成Graphic Buffer
的呢?
其大概流程如图所示:
可以看出,这个过程还是相当复杂的,由于篇幅原因,这里就不展开了,感兴趣的同学可以参阅苍耳叔叔的系列文章:Android图形系统综述(干货篇)
总结
从XML
到View
显示到屏幕上主要涉及到以下知识点
Activity
的启动LayoutInflater
填充View
的原理PhoneWindow
,Activity
,View
的关系Android
窗口机制与WindowManagerService
管理窗口View
的绘制流程,measure
,layout
,draw
等与Handler
同步屏障机制Android
屏幕刷新机制,VSync
信号监听,三级缓冲等Android
图形绘制,包括SurfaceFinger
工作流程,软件绘制,硬件加速等
这篇文章其实已经比较长了,但是要完全了解从XML
到显示到屏幕上的过程,还是不够详细,有很多地方只做了简述,如果有什么错误或者需要补充的地方,欢迎在评论区提出
由于篇幅原因,有一些知识点没有写得很详细,下面列出一些更好的文章供参考:Android
窗口机制:通俗易懂 Android视图系统的设计与实现Android
屏幕刷新机制: “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!Android
图形系统: Android图形系统综述(干货篇)Handler
同步屏障机制:关于Handler同步屏障你可能不知道的问题
参考资料
【Android进阶】这一次把View绘制流程刻在脑子里!!
Android Activity创建到View的显示过程
链接:https://juejin.cn/post/6991483318625632286
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。