我奶奶都能懂的UI绘制流程(上)!
1.前言
从今天开始,慢慢整理Android高级UI的知识,涉及到各种酷炫狂拽吊炸天的特效。
之前写过一篇Window一本满足算是这个专题的预备知识,本文就基于这篇文章,继续往下探索UI的绘制流程。按照国际惯例,我们开始源码的分析,毕竟只有把原理给搞清楚了,才能进行各种天马行空的创作。
2.setContentView
在关于Window的学习中,我们知道每个Activity都有自己的一个Window负责界面的显示。PhoneWindow是抽象Window唯一的实现类。调用Activity的setContentView()
实际上是调用了PhoneWindow的setContentView()
:
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
}
...
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
最开始的时候,判断mContentParent是否为空,为空则执行installDecor()
,从名字上可以看出这个方法与DecorView的初始化有关。接下来通过FEATURE_CONTENT_TRANSITIONS
判断是否需要执行过场动画,需要则执行,不需要则直接通过mLayoutInflater将XML资源加载到mContentParent中。
关于mContentParent和mDecor的关系,直接看官方注释,我就不翻译了。
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
...
}
接着来看看先前猜测的installDecor()
方法到底做了些啥
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
...
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
当mDecor为空时generateDecor()
会直接新建一个DecorView对象并将其返回,注意,DecorView本质上就是一个FrameLayout。
接着当mContentParent为空时,执行generateLayout(mDecor)
并将返回值赋给mContentParent,这是一个重量级的方法,主要包含5块内容
第一步,通过getWindowStyle()
获取当前Window的TypedArray 。熟悉自定义控件的同学对TypedArray一定是相当熟悉的,他可以用来获取布局xml中的信息 。
TypedArray a = getWindowStyle();
第二步,通过获取到的TypedArray对Feature状态位进行设置,比如判断当前Window是否为悬浮状态,是否全屏,是否显示ActionBar,是否透明等等
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
....省略剩下9999种判断...
第三步,通过设置好的Feature获取对应的layoutResource,这些layoutResource都是Android系统原先就提供好的。
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
....省略中间9999种判断...
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
我们来看看最简单的R.layout.screen_simple
布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
ViewStub
是用来延迟加载的一种组件,是用来动态显示bar的,而id为content
的这个FrameLayout
就是我们真正加载布局的地方了。一定要记住android:id="@android:id/content"
,其他类型的布局或许样式不同,但真正加载用户布局的id始终都为content
。
第四步,将获取到的layoutResource进行渲染,添加到decor中。要注意,这个时候用户的布局还没有加载到content中,此时只是将原始的layoutResource加载到decor中
View in = mLayoutInflater.inflate(layo utResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
第五步,获取layoutResource中id为ID_ANDROID_CONTENT
的ViewGroup,并将其返回。这个ViewGroup就是真正加载用户布局的地方。
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
到此为止,generateLayout(mDecor)
完成了自己的历史使命,mContentParent 成为了真正加载用户布局的FrameLayout。回到setContentView()
方法中,现在容器已经准备好了,我们可以放心的开始加载用户布局。
3.LayoutInflater
在setContentView()
的最后,用户布局开始进行加载
mLayoutInflater.inflate(layoutResID, mContentParent);
inflate()
方法第一次重载后如下
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
注意第三个参数为root != null
,由于我们将mContentParent
作为root传入,所以此时第三个参数为true
下一次重载中,会获取一个XmlResourceParser用于解析用户传入的布局资源,之后将这个XmlPullParser 作为参数进行最后一次重载。这个方法内容比较多,我们一点一点看
首先,根据XmlResourceParser获取到AttributeSet ,这个set中保存了xml布局中的配置信息。
final AttributeSet attrs = Xml.asAttributeSet(parser);
接着,通过XML解析获取根节点,此时name就是根节点标签的名字。不熟悉XML解析的同学自行百度。(主要有PULL,SAX,DOM三种解析方式)
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
获取到根节点的标签后,首先要判断是否为TAG_MERGE
。如果是且root为空则抛出异常,否则进行合并渲染。
这里稍微解释一下TAG_MERGE
。在我们写布局的时候,会使用<include/>标签来引入某个布局,<merge/>标签的作用就体现在此,因为父布局已经存在一个ViewGroup了,所以使用<merge/>时,子布局可以不写最外层的ViewGroup。这样就做到了减少图层的效果。
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
}
如果根节点标签不是TAG_MERGE
,那么此时获取到的Tag就是xml中真正的根View。
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs)
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
我们将其创建出来。此时View类已经实例化了,但是在xml设置的属性还没有添加进去。
xml中的属性通过XmlResourceParser解析到attrs中,所以此时要通过root.generateLayoutParams(attrs)
将attrs转化成LayoutParams 。还记得root是什么吗?在上文中,root就是id为content
的那个FrameLayout。是加载用户布局的地方。
我们获取到了LayoutParams,最后只要通过temp.setLayoutParams(params)
将params属性设置到View中就OK了。继续往下看代码:
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
现在已经获得了用户布局中的根View以及它的属性,接下来就通过rInflateChildren(parser, temp, attrs, true)
来渲染其子view,其会重载rInflate()
,这个方法长度适中,为了大家能全方位的理解,就一口气展示出来
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
...
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
...
else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();
}
}
可以看到,整个过程是处于while循环中,这也是xml解析的一种基本方式。
首先还是通过XmlPullParser 获取到子布局的名称,接着开始判断子布局的类型。如果类型为TAG_INCLUDE
并且深度为0,说明<include />是根节点,抛出异常。如果发现类型为TAG_MERGE
且深度不为0,说明<merge />不是根节点,抛出异常。
异常判断结束后,重复之前绘制根节点的操作,将子View与子View的子View都一一绘制并添加到他们的父View中。
经过上面这些操作后,用户界面XML中的元素就全部解析并且封装了起来,最后就可以调用root.addView(temp, params)
将这个封装完毕的View添加到root中。
到此为止,LayoutInflater.inflate()
方法完成了它的历史使命,我们用一张图来总结
4.AppCompatActivity
文章前面已经将Activity的setContentView()
介绍完毕了,但是现在使用AndroidStudio开发时,咱们默认的Activity是谁?是AppCompatActivity,这是一个为了填补Google曾经挖下的各种坑而出现的超级无敌自适应Activity。下面,大家一起来看看AppCompatActivity的setContentView()
是怎么操作的。
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
这一上来就不一样啊。
getDelegate()
是获取代理的方法,会通过mDelegate = AppCompatDelegate.create(this, this)
来创建代理对象
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
可以看到,不同的版本会返回不同的代理对象,这些代理对象都继承自AppCompatDelegateImplV9
,我们重点看这个类的setContentView()
方法。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
其他内容都和Activity中的差不多,就是第一行多了ensureSubDecor()
,这个方法会调用createSubDecor()
来创建一个ViewGroup对象,这是AppCompatActivity中十分关键的一个方法。
createSubDecor()
和Activity中的generateLayout(mDecor)
十分类似,因为比较重量级,具体的可以结合源码与文章前面对generateLayout(mDecor)
的分析来看,在这里我们就分析几处关键的地方。
首先,获取到TypedArray 对象。
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
要注意,此处的主题是R.styleable.AppCompatTheme
。系统需要通过这个主题来对一些View进行兼容性的改造。这也就是为什么在使用AppCompatActivity时,主题必须设置为AppCompat类型,否则就会抛出异常。
接下来,获取DecorView
// Now let's make sure that the Window has installed its decor by retrieving it
mWindow.getDecorView();
getDecorView()
会调用PhoneWindow的installDecor()
,这个方法之前详细介绍过,很重要,忘记了就往前翻翻。
继续下潜,有很长一段代码都是用来判断subDecor需要加载什么系统布局,这个过程和Activity中的类似,我们依然以simple布局为例
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.FitWindowsLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/action_bar_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<android.support.v7.widget.ViewStubCompat
android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/abc_action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include layout="@layout/abc_screen_content_include" />
</android.support.v7.widget.FitWindowsLinearLayout>
这些组件都是在v7包中用来做版本适配的,再来看看Include进来的这个布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</merge>
这个ContentFrameLayout就相当于Activity中的FrameLayout,所以我们一定要把它的id记住,action_bar_activity_content
,action_bar_activity_content
,action_bar_activity_content
,说三遍。
终于,材料已经准备完毕,是时候来享受真正的大餐了。
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
contentView
是subDecor中id为action_bar_activity_conten
t的ContentFrameLayout,
windowContentView
是mWindow中id为content
的FrameLayout。
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
...
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
将windowContentView的id设为NO_ID
,再将contentView的id设为content
。这个时候,R.id.action_bar_activity_content
就完成了它的任务。
最后,将subDecor
添加到mWindow中,大功告成!
是不是感觉茅塞顿开了?这招偷梁换柱简直漂亮!我们上一张图来感受此时下整体的结构。
AppCompatActivity的View布局结构.png5.ViewRootImpl
仔细回忆下之前的过程,在setContentView()
方法中,界面布局的xml资源已经解析并生成了view,而view也添加到了window上,但此时view并没有绘制出来,对用户而言还是不可见的。
接下来,我们就来学习View的绘制流程。在开始前,强烈建议大家先去复习下有关Window的爱恨情仇!以及Activity启动流程简直丧心病狂!,不然等会懵逼的可能性会很大。
故事要从Activity启动流程简直丧心病狂!的结尾开始,上回说到,在ActivityThread中调用了handleLaunchActivity()
开始真正启动一个活动,今天咱们就来仔细分析下这个方法。
Activity a = performLaunchActivity(r, customIntent);
首先,调用performLaunchActivity()
实例化了Activity,这个方法主要做了三件事
第一,通过反射获取到Activity的实例
ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
第二,调用Activity.attach()
初始化window
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
final void attach(...){
...
mWindow = new PhoneWindow(this, window);
...
}
第三,通过mInstrumentation最终回调了Activity的onCreate方法
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState)
public void callActivityOnCreate(Activity activity, Bundle icicle,
PersistableBundle persistentState) {
...
activity.performCreate(icicle, persistentState);
...
}
final void performCreate(Bundle icicle) {
...
onCreate(icicle);
...
}
由于setContentView()
是在onCreate()
中执行的,所以现在我们就获取了view并添加到了window上,接下来要开始绘制了,很显然,留给我们进行绘制的只剩下onResume
。
现在回到handleLaunchActivity()
方法中,继续往下看,果然这里会调用handleResumeActivity()
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
进入这个方法,看看会发生什么。
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
...
wm.addView(decor, l);
也就是说,在handleResumeActivity()
中,我们获取到了DecorView以及WindowManager,并将decor添加到了wm中。
下面这些内容在有关Window的爱恨情仇!中有详细的介绍,这里简要的说一下
WindowManager.addView()
的作用就是通过AIDL将window显示到屏幕上,再调用ViewRootImpl进行view的绘制
在addView()
中,会实例化ViewRootImpl对象并调用它的setView()
方法
root = new ViewRootImpl(view.getContext(), display);
...
root.setView(view, wparams, panelParentView);
ViewRootImpl.setView()
主要做了三件事,第一是通过下面的代码将window添加到屏幕上
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
第二是调用requestLayout()
进行view的绘制
第三是调用view.assignParent(this)
将decorView的parent设置为当前的ViewRootImpl
事件一在有关Window的爱恨情仇!介绍过了,过程比较复杂,请移步。
今天我们主要介绍事件二、三。
首先看比较简单的事件三,这里就是直截了当的将ViewRootImpl设置为decorView的parent
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
}
...
}
这么做的意义是什么呢?大家知道,在View中调用requestLayout()
会使得界面重绘,来看看这个方法
public void requestLayout() {
...
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
...
}
原来如此,View.requestLayout()
会不断回调其parent的requestLayout()
方法,最后到达decorView时,就会调用ViewRootImpl的requestLayout()
。
也就是说,ViewRootImpl.requestLayout()
是view绘制的起源,我们来事件二仔细感受一下
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
该方法会调用scheduleTraversals()
void scheduleTraversals() {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
接着在mChoreographer中执行mTraversalRunnable,这是一个Runnable 对象,唯一的作用就是调用doTraversal()
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
doTraversal()
又会调用performTraversals()
,这个方法那是相当长,一看就是有特殊癖好的变态工程师写的,我们主要看其中与UI绘制有关的部分。从前往后慢慢找,依次可以看到他们:
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performDraw();
哇!终于看到这三兄弟了!大家一起来松口气,咱们今天就说到这,虽然还没开始View的绘制,但前面的准备工作都完成啦!最后方式一张流程图进行来梳理一下吧。
DecorView添加至窗口的过程.png6.总结
什么?你说让我奶奶懂一个给你看看?
哈哈,哈哈,哈哈哈……
完结撒花~