您的位置:首页 > 产品设计 > UI/UE

Android UI绘制流程

2017-12-14 09:36 555 查看
本文从源码(基于MTK Android 7.0)角度分析 Android 中 View 的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把 View 绘制的整个流程说明清楚,从而帮助大家对特定实现细节的相应源码进行研读。

在进行实际的分析之前,先来看下面这张图:



1.1 View 加载的整体流程

View 的加载从 Activity 的 setContentView() 方法开始,通过 PhoneWindow 实例化一个 DecorView,将 setContentView()
设置的视图填充到 DecorView 中 id 是 android.R.id.content 的 FarmeLayout 里面显示,再把 DecorView 添加到 WindowManager 管理,最后由根视图 ViewRoot 设置 View 显示在手机上。

1.2
理解 Window


Window
是 PhoneWindow 的父类,其分为三种类型:系统 Window、应用程序 Window 和子 Window。每个 Window
都会指定一个 type 值,这是一个比较重要的概念,指窗口的类型。

系统
Window:比如在手机电量低的时候,会有一个提示电量低的 Window;输入文字的时候,会弹出输入法 Window;还有搜索条
Window;来电显示 Window;Toast 对应的 Window。可以总结出来,系统 Window 是独立与我们的应用程序的,对于应用程序而言,我们理论上是无法创建系统
Window,因为没有权限,这个权限只有系统进程有。所对应的层级区间是 2000 以上。

应用程序 Window:比如 Activity 就是一个应用程序 Window,源码中给到的 type 值是 TYPE_BASE_APPLICATION
。所对应的层级区间是 1 - 99。

子 Window:所谓的子Window,是说这个Window必须要有一个父窗体,比如 PopWindow ,Dialog
等等 。所对应的层级区间是 1000 - 1999。

那么每个层级具体有那些,请看 WindowManager.LayoutParams 中的 type 值。

1.3
理解 PhoneWindow


PhoneWindow
是 Android 系统中最基本的窗口系统,每个 Activity 会创建一个。PhoneWindow 是 Activity 和 View 系统交互接口。DecorView 本质上是一个 FrameLayout,是 Activity 中所有 View 的父类。

1.4
setContentView 过程


Activity
的 setContentView() 方法本质上调用的是 PhoneWindow 的 setContentView()方法,其有 3 个重载方法,核心代码如下:
public void setContentView(int layoutResID) {
installDecor();
...
mLayoutInflater.inflate(layoutResID, mContentParent);
...
}

public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

public void setContentView(View view, ViewGroup.LayoutParams params) {
installDecor();
...
mContentParent.addView(view, params);
...
}
如果传入参数是 layoutResID,则需要先解析成 View,再添加到 WindowManager 里面。如果参数传入 View,则直接添加到 WindowManager。

1.5 installDecor
过程


private void installDecor() {
...
// 返回 DecorView 的实例,其中 DecorView 是 PhoneWindow 的内部类
mDecor = generateDecor(-1);
...
// 初始化 Activity 主题,根据获取到的 style 对 Activity 窗口属性特征的设置,并返回 ContentView。
mContentParent = generateLayout(mDecor);
}
在 generateLayout()
方法中,初始化 activity 的主题后,接着会根据 features 值获取不同的 layoutResource。并将 layoutResource 加载到 DecorView 里面,其中
id 是 android.R.id.content 的 ViewGroup 则赋值给 ContentView 。所以开发者设置 requestFeature() 的时候要在 setContentView() 之前调用。系统有很多种 layoutResource,这里只看一看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>
从代码可以看出,DecorView
的布局文件里面分为两个部分,ViewStub 和 FrameLayout。而开发者写的 layout 都会填充到 id 为 android.R.id.content
的 FrameLayout 里面。

1.6 inflate 过程

如果传入参数是 layoutResID,则需要先解析成 View。这时会调用 LayoutInflater 的 inflate() 方法。核心代码如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
if (TAG_MERGE.equals(name)) {
// 如果使用 merge 标签,必须设置其为根节点 并且设置 attachToRoot=true
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);
}
....
}

void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
...
final String name = parser.getName();

if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
// 如果使用 include 作为根节点,抛出异常
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
// 如果使用了 merge作为子节点,抛出异常
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);
// 解析子 View, 方法里面依然调用 rInflate
rInflateChildren(parser, view, attrs, true);
// 将 View 添加到 ViewGroup
viewGroup.addView(view, params);
}
...
}


1.7 WindowManager
过程


WindowManager
是一个接口,其子类 WindowManagerImpl 通过 addView() 方法对 DecorView 添加管理,然后通过 ViewRootImpl 的 setView() 方法设置 View 显示。核心代码如下:
// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
root.setView(view, wparams, panelParentView);
}

// ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
//  最终会调用到 doTraversal()
requestLayout();
...
}

void doTraversal() {
...
performTraversals();
...
}


2.1 View 绘制的整体流程

View 的绘制会从根视图 ViewRoot 的 performTraversals() 方法开始,从上到下遍历整个视图树,每个
View 控件负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作。视图绘制的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)。

performTraversals()
的核心代码如下:
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// 执行测量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
// 执行布局流程
performLayout(lp, mWidth, mHeight);
...
// 执行绘制流程
performDraw();
}


2.2
理解 MeasureSpec 

为了更好地理解 View 的测量过程,我们还需要了解 MeasureSpec。MeasureSpec 表示的是一个 32 位的整型值,它的高 2 位表示测量模式 SpecMode,低 30 位表示某种测量模式下的规格大小
SpecSize。MeasureSpec 是 View 类的内部静态类,用来说明应该如何测量这个 View,其核心代码如下:
/**
* MeasureSpec 封装了父布局传递给子布局的布局要求,每个 MeasureSpec 代表了一组宽度和高度的要求
* MeasureSpec 由 size 和 mode 组成。
* MeasureSpecs 使用了二进制去减少对象的分配。
*/
public static class MeasureSpec {
// 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)
private static final int MODE_SHIFT = 30;
// 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
// (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

// UNSPECIFIED 模式:父 View 不对子 View 有任何限制,子 View 需要多大就多大
// 在源码中的处理和 EXACTLY 一样。当 View 的宽高值设置为 0 的时候或者没有设置宽高时,模式为 UNSPECIFIED
// 0向左进位30,就是00 00000000000(00后跟30个0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

// EXACTYLY 模式:父 View 已经测量出子 Viwe 所需要的精确大小,这时候 View 的最终大小
// 当设置 width 或 height 为 match_parent 时,模式为 EXACTLY,因为子 View 会占据剩余容器的空间,所以它大小是确定的
// 1向左进位30,就是01 00000000000(01后跟30个0)
public static final int EXACTLY     = 1 << MODE_SHIFT;

// AT_MOST 模式:子 View 的最终大小是父 View 指定的 SpecSize 值,并且子 View 的大小不能大于这个值
// 当设置为 wrap_content 时,模式为 AT_MOST, 表示子 View 的大小最多是多少,这样子 View 会根据这个上限来设置自己的尺寸
// 2向左进位30,就是10 00000000000(10后跟30个0)
public static final int AT_MOST     = 2 << MODE_SHIFT;

// 根据提供的 size 和 mode 得到一个详细的测量结果。measureSpec = size + mode;(注意:二进制的加法,不是10进制的加法!)
// 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
// 例如:size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

// 通过详细测量结果获得 mode。mode = measureSpec & MODE_MASK;
// MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
// 例如:10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}

// 通过详细测量结果获得 size。size = measureSpec & ~MODE_MASK;
// 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

// 微调某个 MeasureSpec 的大小
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}

// 重写的toString方法,打印mode和size的信息
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);

StringBuilder sb = new StringBuilder("MeasureSpec: ");

if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");

sb.append(size);
return sb.toString();
}
}


以上,我们需要重点关注代码中的以下三种模式,这个在 Measure 阶段用到。

·UNSPECIFIED:不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。例如:ListView

·EXACTLY:精确测量模式,当该视图的 layout_width 或者 layout_height 指定为具体的数值或者 match_parent 时生效,表示父视图已经决定了子视图的精确大小,这种模式下 View 的测量值就是 SpecSize 的值。

·AT_MOST:最大值模式,当该视图的 layout_width 或者 layout_height 指定为 wrap_content 时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。

对 DecorView 而言,它的 MeasureSpec 由窗口尺寸和其自身的 LayoutParams 共同决定;对于普通的 View,它的 MeasureSpec 由父视图的 MeasureSpec 和自身的 LayoutParams 共同决定。

2.3
Measure 过程


Measure
操作用来计算 View 的实际大小,由前面的分析可以知道,视图的测量流程是从 performMeasure() 方法开始的,核心代码如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
...
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
具体的测量操作是分发给 ViewGroup 的,由 ViewGroup 在它的 measureChild() 方法中传递给子 View,代码如下。ViewGroup 通过遍历自身所有的子
View,并逐个调用子 View 的 measure() 方法实现测量操作。

// 遍历测量 ViewGroup 中所有的 View
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 当 View 可见性处于 GONE 状态时,不对其进行测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

// 根据父容器的 MeasureSpec 和子 View 的 LayoutParams 等信息计算子 View 的 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}下面来看看
View(ViewGroup)的 measure() 方法,最终测量时通过回调 onMeasure() 方法实现的,这个通常由 View 的特定子类自己实现,开发者也可以通过重写这个方法实现自定义 View。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}

// 如果需要自定义测量过程,则子类可以重写这个方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// setMeasuredDimension 方法用于设置 View 的测量宽高
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// 如果有 View 没有重写 onMeasure() 方法,则默认会直接调用 getDefaultSize() 来获得 View 的宽高
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

2.4 Layout 过程

Layout 过程用来确定 View 在父容器中的布局位置,它由父容器获取子 View 的位置参数后,调用子 View 的 layout() 方法并将位置参数传入实现的,ViewRootImpl 的 performLayout() 代码如下:
// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}

// View.java
public void layout(int l, int t, int r, int b) {
...
onLayout(changed, l, t, r, b);
...
}

// 空方法,子类如果是 ViewGroup 类型,则重写这个方法,实现 ViewGroup 中所有的 View 控件布局流程
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
当 ViewGroup 的位置被确定后,它在 onLayout() 中会遍历所有的子元素并调用其 layout()  方法,在 layout()
方法中 onLayout() 又会被调用。在 ViewGroup 和 View 里面均没有实现 onLayout() 方法,该方法由其子类重写,这个方法的用途是父容器确定子元素的位置。

2.5 Draw 过程

Draw 操作用来将控件绘制出来,绘制的流程从 performDraw() 开始,核心代码如下:
private void performDraw() {
...
draw(fullRedrawNeeded);
...
}

private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
可以看到最终调用到每个 View 的 draw() 方法绘制每个具体的 View,View 的绘制遵循六个步骤,代码如下:

public void draw(Canvas canvas) {
...
// 步骤一:绘制 View 的背景
drawBackground(canvas);

...
// 步骤二:如果需要的话,保存 canvas 的图层,为 fading 做准备
saveCount = canvas.getSaveCount();
...
canvas.saveLayer(left, top, right, top + length, null, flags);

...
// 步骤三:绘制 View 的内容
onDraw(canvas);

...
// 步骤四:绘制 View 的子 View
dispatchDraw(canvas);

...
// 步骤五:如果需要的话,绘制 View 的 fading 边缘并恢复图层
canvas.drawRect(left, top, right, top + length, p);

...
// 步骤六:绘制 View 的装饰(例如:滚动条)
onDrawScrollBars(canvas);
}最后看一张整个 draw() 的递归流程图

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: