View绘制流程--Based on kitkat
从Acitivty的启动开始,我们就看到setContentView(见从setContentView()谈起)是如何创建和初始化的,但不清楚视图View如何添加到窗口以及绘制到窗口的,那下面我们就一起来看一下视图View是如何绘制的。
1. View绘制的触发
可能很多人看过一些文章或者书籍,大概都知道ViewRootImpl.performTraversals()是绘制的开始关键方法调用,可这个方法是怎么调用到的呢,他的流程是怎么样的呢?接下来我们就一起看下View绘制是如何触发的。
首先我们从Activity的启动中就可以看到调用ActivityThread.handleLaunchActivity(ActivityClientRecord r, Intent customIntent),在这个函数里面有两个关键的调用:
- 调用performLaunchActivity来进行Activity的创建和View的初始化(setContentView相关)
- 调用handleResumeActivity将视图View添加到窗口并绘制
public final class ActivityThread {
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
……
1. 调用performLaunchActivity来进行Activity的创建和View的初始化(setContentView相关)
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
2. 调用handleResumeActivity将视图View添加到窗口并绘制
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
……
} else {
……
}
}
}
这两个调用之后我们可以看到了界面了,而界面是怎么绘制的,我们来通过另外一张顺序图看下绘制是如何触发的。
public final class ActivityThread {
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
boolean reallyResume) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
1. 调用performResumeActivity执行resume相关操作,最终会调用到activity的onResume
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
if (localLOGV) Slog.v(
TAG, "Resume " + r + " started activity: " +
a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ ", finished: " + a.mFinished);
final int forwardBit = isForward ?
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
2. 调用WindowManagerImpl的addView方法将performLaunchActivity的过程创建的DecorView对象decor与WMS关联起来
wm.addView(decor, l);
}
……
}
……
// Tell the activity manager we have resumed.
if (reallyResume) {
try {
ActivityManagerNative.getDefault().activityResumed(token);
} catch (RemoteException ex) {
}
}
} else {
……
}
}
}
从上图里面我们看到handleResumeActicvity通过调用WindowManagerImpl的addView方法将performLaunchActivity的过程创建的DecorView对象decor与Window关联起来。addView通过一系列的调用,最终调用ViewRootImpl.performTraversals(),从而触发了绘制,到此View绘制得以触发。
2. ViewRootImpl.performTraversals()详细分析
performTraversals函数是进行View的遍历的核心函数。该函数逻辑很清晰,主要分为4大步骤:
- Step 1. 创建Surface,并打通native层(relayoutWindow)
- Step 2. 计算视图大小(performMeasure)
- Step 3. 布局,将视图放置在合适位置(performLayout)
- Step 4. 绘制(performDraw)
在relayoutWindow之前还有很长一段代码,这段代码到底做了什么呢,下面我们就先分析下。
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
……
1. 初始化attchInfo
final View.AttachInfo attachInfo = mAttachInfo;
……
Rect frame = mWinFrame;
2. 判断是否是第一次遍历
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
……
3. 如果是第一次遍历,就调用dispatchAttachedToWindow,所有子视图都把attachInfo的值复制到自己的mAttachInfo中
host.dispatchAttachedToWindow(attachInfo, 0);
attachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets);
host.fitSystemWindows(mFitSystemWindowsInsets);
//Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn);
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
4. 如果不是第一次,判断窗口大小是否有变化,如果有,则会将下面三个变量置为true。
- mFullRedrawNeeded = true,需要全部重绘
- mLayoutRequested = true,需要重新布局即重新为视图指定位置
- windowSizeMayChange = true,窗口大小可能改变
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(TAG,
"View " + host + " resized to: " + frame);
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
5. 如果visibility发生变化,将调用host.dispatchWindowVisibilityChanged将这个变化通知给所有的子视图
if (viewVisibilityChanged) {
attachInfo.mWindowVisibility = viewVisibility;
host.dispatchWindowVisibilityChanged(viewVisibility);
if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
destroyHardwareResources();
}
if (viewVisibility == View.GONE) {
// After making a window gone, we will count it as being
// shown for the first time the next time it gets focus.
mHasHadWindowFocus = false;
}
}
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(attachInfo.mHandler);
boolean insetsChanged = false;
boolean layoutRequested = mLayoutRequested && !mStopped;
if (layoutRequested) {
……
// Ask host how big it wants to be
6. 测量判断window的大小是否需要改变
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
……
boolean hwInitialized = false;
boolean contentInsetsChanged = false;
boolean hadSurface = mSurface.isValid();
try {
if (DEBUG_LAYOUT) {
Log.i(TAG, "host=w:" + host.getMeasuredWidth() + ", h:" +
host.getMeasuredHeight() + ", params=" + params);
}
final int surfaceGenerationId = mSurface.getGenerationId();
7. 重新分配窗口大小,创建Surface,并打通native层
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
……
}
}
Step1. 重新分配窗口大小,创建Surface,并打通native层(relayoutWindow)
在relayoutWindow调用中会去调用WMS的relayoutWindow来调整窗口属性并将上次Surface对象与底层Surface打通。我们可以从下图中清晰地看出打通的过程:
- 创建底层surface对象
- 通过copyFrom将native层和上层的Surface对象关联
Step2. 计算视图大小(performMeasure)
首先,Android系统希望程序员能够理解画布(Canvas)是没有边界的,即无穷大。程序员可以在画布上绘制任意多任意大的东西(只要内存足够)。我们在配置视图布局的时候可以配置具体的值(比如250dp),可以配置相对值(比如WRAP_CONTENT、MATCH_PARENT),measure过程就是把视图布局中的相对值转换为具体值,最后保存在mMeasuredWidth和mMeasureHeight中。
measure过程中一个关键调用就是View.measure,该方法是final的,因此不能被重载,该函数会回调onMeasure方法,如果我们自定义的View,这边就是MyView,是一个View的子类,则执行MyView的onMeasure或者View的onMeasure,如果MyView没有重写这个方法;如果MyView是一个ViewGroup的对象则在重写onMeasure方法的时候需要调用ViewGroup的measureChildWithMargins来对它的子视图进行计算。
View.measure方法调用时候的两个参数widthMeasureSpec和heightMeasureSpec对应MeasureSpec的是视图大小计算时候的说明,视图的大小由父视图和子视图共同决定,而这个说明里面包含了父视图的大小和specMode。specMode有如下三种:
- MeasureSpec.EXACTLY:“确定的”,父视图希望子视图的大小是specSize中指定的值。
- MeasureSpec.AT_MOST:“最多”,子视图的大小最多是specSize的值
- MeasureSpec.UNSPECIFIED:“没有限制”,View的设计者可以根据自身特性设置视图大小
Step 3. 布局,将视图放置在合适位置(performLayout)
layout的目的就是父视图按照子视图的大小和布局参数,将子视图放置到合适的位置上。布局过程主要是通过调用View.layout实现的。该函数主要做了三件事情:
1. 调用setFrame将位置的参数保存起来。如果这些值跟以前的相同则什么也不做,如果不同则进行重新赋值,并在赋值前,会给mPrivateFlags添加PFLAG_DRAWN的标识,同时调用invalidate告诉系统View视图原先占用的位置需要重绘。
2. 回调onLayout,View本身的onLayout什么也不做,提供此方法是为了方便ViewGroup进行重写来对它的子视图进行布局。需要了解详细onLayout实现可以看下LinearLayout、RelativeLayout等ViewGroup的onLayout实现。
Step 4. 绘制(performDraw)
绘制顾名思义就是把视图View对象绘制到屏幕上。**每次重绘的时候并不会重新绘制每个View树的视图,而只是绘制那些“需要重绘”的,也就是mPrivateFlags中含有PFLAG_DRAWN标识的视图**
由上图我们可以看出绘制过程经过一系列的调用和准备,其中之一就是检查Surface的有效性,此Surface就是前文relayoutWindow过程中创建的Surface,最终会调用到mView.draw,该函数主要做了6件事:
1. 绘制背景,每个视图都有一个背景,可以是一个颜色值,也可以是一幅图片,甚至是任何Drawable对象,
2. 如果需要,保存画布的层准备用于渐变
3. 绘制View的内容,对于TextView而言,内容就是文字,对于ImageButton而言,内容就是一副图片;程序会在onDraw方法中绘制具体的内容
4. 绘制View的子视图
5. 如果需要,绘制渐变框和恢复画布层,渐变框作为是为了让视图View看起来更有层次感,其本质是一个Shader对象
6. 绘制装饰(比如滚动条)