Android View的工作流程

2018-01-13 11:06:51来源:oschina作者:微笑的江豚人点击

分享

前言
ViewRootImpl

performTraversals
measure

MeasureSpec
DecorView performTraversals
measureHierarchy
performMeasure
measure
onMeasure ViewGroup - FrameLayout - onMeasure
View - onMeasure

layout

小结drawinvalidate

View的脏区域和实心控件
View
ViewGroup
ViewRootImplrequestLayout
总结
参考
前言

在上一篇Window机制探索中我们知道,ViewRootImpl在整个View体系中起着中流砥柱的作用,它是控件树正常运作的动力所在,并且有如下几个重要功能点。

连接WindowManager和DecorView的纽带。
向DecorView派发输入事件
完成View的绘制(measure,layout,draw)。
负责与WMS交互通讯,调整窗口大小及布局。
ViewRootImpl

这里写图片描述

我们沿用Window机制探索中Window的添加流程图,我们所要分析的绘制机制,便从ViewRootImpl的setView()方法展开。


//ViewRootImpl public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
}@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检查是否在主线程
mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
scheduleTraversals();
}
}void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//post一个runnable处理-->mTraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
``````
}
}final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();void doTraversal() {
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();//View的绘制流程正式开始。
}

在scheduleTraversals方法中,通过mHandler发送一个runnable,在run()方法中去处理绘制流程,这一点和ActivityThread的H类相似,因为我们知道ViewRootImpl中W类是Binder的Native端,用来接收WmS处理操作,因为W类的接收方法是在线程池中的,所以我们可以通过Handler将事件处理切换到主线程中。

performTraversals

ViewRootImpl在其创建过程中通过requestLayout()向主线程发送了一条触发遍历操作的消息,遍历操作是指performTraversals()方法。它是一个包罗万象的方法。ViewRootImpl中接收的各种变化,如来自WmS的窗口属性变化、来自控件树的尺寸变化及重绘请求等都引发performTraversals()的调用,并在其中完成处理。View类及其子类中的onMeasure()、onLayout()、onDraw()等回调也都是在performTraversals()的执行过程中直接或间接的引发。也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊的工作,一旦此方法无法正常执行,整个控件树都将处于僵死状态。因此performTraversals()函数可以说是ViewRootImpl的心跳。


View首次绘制流程 View首次绘制流程

首先我们看一下performTraversals()首次绘制的大致流程,如上图所示:performTraversals()会一次调用performMeasure、performLayout、performDraw三个方法,这三个方法便是View绘制流程的精髓所在。

performMeasure: 会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传到子元素中了,这样就完成了一次measure过程。measure完成以后,可以通过getMeasuredWidth和getMeasureHeight方法来获取到View测量后的宽高。


performLayout: 和performMeasure同理。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成以后,可以通过getTop/Bottom/Left/Right拿到View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View的最终宽高。


performDraw: 和performMeasure同理,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

我们知道,View的绘制流程是从顶级View也就是DecorView「ViewGroup」开始,一层一层从ViewGroup至子View遍历测绘,我们先纵观performTraversals()全局,认识View绘制的一个整体架构,后面我们会补充说明部分重要代码。如 : view.invalidate、view.requestLayout、view.post(runnable)等。


//ViewRootImplprivate void performTraversals() {
//调用performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
performLayout(lp, mWidth, mHeight);
performDraw();
} measure
MeasureSpec

View的测量过程中,还需要理解MeasureSpec,MeasureSpec决定了一个View的尺寸规格,并且View的MeasureSpec受自身的LayoutParams(一般是xml布局中width和height)和父容器MeasureSpec的影响。


MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。

SpecMode: 测量模式,有UNSPECIFIED、 EXACTLY、AT_MOST三种。
SpecSize: 在某种测量模式下的尺寸和大小。

SpecMode有如下三种取值:

UNSPECIFIED : 父容器对子View的尺寸不作限制,通常用于系统内部。(listView和scrollView等)


EXACTLY :SpecSize表示View的最终大小,因为父容器已经检测出View所需要的精确大小,它对应LayoutParams中的match_parent和具体的数值这两种模式。


AT_MOST :SpecSize表示父容器的可用大小,View的大小不能大于这个值。它对应LayoutParams中的wrap_content。

MeasureSpec和LayoutParams


子View的MeasureSpec==LayoutParams+margin+padding+父容器的MeasureSpec


对于普通View(DecorView略有不同),其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。


那么针对不同父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec,我们从子View的角度来分析。

①View宽/高采用固定宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY并且其大小遵循LayoutParams中的大小。


②View宽/高采用wrap_content的时候 ,不管父容器的模式是EXACTLY还是AT_MOST,View的模式总是AT_MOST,并且大小不能超过父容器的剩余空间(SpecSize,可用大小)。


③View宽/高采用match_parent的时候 : I如果父容器的模式是EXACTLY,那么View也是EXACTLY并且其大小是父容器的剩余空间(SpecSize,最终大小); II如果父容器的模式是AT_MOST,那么View也是AT_MOST并且其大小不会超过父容器的剩余空间(SpecSize,可用大小)。


这里写图片描述 Android开发艺术探索


DecorView

有了上面的理论知识铺垫之后,我们来看一下DecorView的measure过程。


performTraversals
//ViewRootImpl private void performTraversals() {
final View host = mView;
int desiredWindowWidth;//decorView宽度
int desiredWindowHeight;//decorView高度
if (mFirst) {
if (shouldUseDisplaySize(lp)) {
//窗口的类型中有状态栏和,所以高度需要减去状态栏
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
//窗口的宽高即整个屏幕的宽高
Configuration config = mContext.getResources().getConfiguration();
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
}
//在onCreate中view.post(runnable)和此方法有关
host.dispatchAttachedToWindow(mAttachInfo, 0);
} boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
``````
//创建了DecorView的MeasureSpec,并调用performMeasure
measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
}

在ViewRootImpl中,我们如上分析一下主要代码,在measureHierarchy()方法中,创建了DecorView的MeasureSpec。


measureHierarchy
//ViewRootImplprivate boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
boolean goodMeasure = false;
//针对设置WRAP_CONTENT的dialog,开始协商,缩小布局参数
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// On large screens, we don't want to allow dialogs to just
// stretch to fill the entire width of the screen to display
// one line of text.First try doing the layout at a smaller
// size to see if it will fit.
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
goodMeasure = true;
}
``````
if (!goodMeasure) {//DecorView,宽度基本都为match_parent
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
} return windowSizeMayChange;
}//创建measureSpec
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

measureHierarchy()用于测量整个控件树,传入的参数desiredWindowWidth和desiredWindowHeight在前面方法中根据当前窗口的不同情况(状态栏)挑选而出,不过measureHierarchy()有自己的考量方法,让窗口布局更优雅,(针对wrap_content的dialog),所以设置了wrap_content的Dialog,有可能执行多次测量。(DecorView的xml布局中,宽高基本都为match_parent)

这里写图片描述

通过上述代码,DecorView的MeasureSpec的产生过程就很明确了,具体来说其遵守如下规则,根据它的LayoutParams中的宽高的参数来划分 : (与上面所说的MeasureSpec和LayoutParams同理)

LayoutParams.MATCH_PARENT : EXACTLY(精确模式),大小就是窗口的大小;


LayoutParams.WRAP_CONTENT : AT_MOST (最大模式),大小不确定,但是不能超过窗口的大小,暂定为窗口大小;


固定大小(写死的值) : EXACTLY(精确模式),大小就是当前写死的数值。


performMeasure
//ViewRootImpl -->measureHierarchy
//该方法很简单,直接调用mView.measure()方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

在view.measure()的方法里,仅当给与的MeasureSpec发生变化时,或要求强制重新布局时,才会进行测量。


强制重新布局 : 控件树中的一个子控件内容发生变化时,需要重新测量和布局的情况,在这种情况下,这个子控件的父控件(以及父控件的父控件)所提供的MeasureSpec必定与上次测量时的值相同,因而导致从ViewRootImpl到这个控件的路径上,父控件的measure()方法无法得到执行,进而导致子控件无法重新测量其布局和尺寸。


解决途径 : 因此,当子控件因内容发生变化时,从子控件到父控件回溯到ViewRootImpl,并依次调用父控件的requestLayout()方法。这个方法会在mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利执行,进而这个子控件有机会进行重新布局与测量。这便是强制重新布局的意义所在。



measure
//View
//final类,子类不能重写该方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
``````
onMeasure(widthMeasureSpec, heightMeasureSpec);
}//ViewGroup并没有重写该方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

view.measure()方法其实没有实现任何测量的算法,它的作用在于判断是否需要引发onMeasure()的调用,并对onMeasure()行为的正确性进行检查。

onMeasure

ViewGroup是一个抽象类,并没有重写onMeasure()方法,就要具体实现类去实现该方法。因为我们的顶级View是DecorView,是一个FrameLayout,所以我们从FrameLayout开始继续我们的主线任务。

ViewGroup -> FrameLayout -> onMeasure()


//FrameLayout@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//遍历子View,只要子View不是GONE便处理
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//子View结合父View的MeasureSpec和自己的LayoutParams算出子View自己的MeasureSpec
//如果当前child也是ViewGroup,也继续遍历它的子View
//如果当前child是View,便根据这个MeasureSpec测量自己
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
``````
}
}
``````
//父View等所有的子View测量结束之后,再来测量自己
setMeasuredDimension(``````);
}

我们知道ViewGroup的measure任务主要是测量所有的子View,测量完毕之后根据合适的宽高再测量自己。


在FrameLayout的onMeasure()方法中,会通过measureChildWithMargins()方法遍历子View,并且如果FrameLayout宽高的MeasureSpec是AT_MOST,那么FrameLayout计算自身宽高就会受到子View的影响,可能使用最大子View的宽高。


不同ViewGroup实现类有不同的测量方式,例如LinearLayout自身的高度可能是子View高度的累加。


measureChildWithMargins()方法为ViewGroup提供的方法,根据父View的MeasureSpec和子View的LayoutParams,算出子View自己的MeasureSpec。


//ViewGroupprotected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。显然子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin和padding有关,如果对该理论知识印象不太深刻建议滑到上个段落 —-MeasureSpec,再来看以下代码,事半功倍。



子View的MeasureSpec==LayoutParams+margin+padding+父容器的MeasureSpec


//ViewGroup//spec为父容器的MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);//父容器的specMode
int specSize = MeasureSpec.getSize(spec);//父容器的specSize
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {//根据父容器的specMode
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述方法不难理解,它的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。


View -> onMeasure


回顾一下上面的measure段落,因为具体实现的ViewGroup会重写onMeasure(),因为ViewGroup是一个抽象类,其测量过程的onMeasure()方法需要具体实现类去实现,这么做的原因在于不同的ViewGroup实现类有不同的布局特性,比如说FrameLayout,RelativeLayout,LinearLayout都有不同的布局特性,因此ViewGroup无法对onMeasure()做同一处理。


但是对于普通的View只需完成自身的测量工作即可,所以可以看到View的onMeasure方法很简洁。


//View
//final类,子类不能重写该方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
``````
onMeasure(widthMeasureSpec, heightMeasureSpec);
}//ViewGroup并没有重写该方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}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;
}

从getDefaultSize()方法的实现来看,对于AT_MOST和EXACTLY这两种情况View的宽高都由specSize决定,也就是说如果我们直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。


这里写图片描述


layout

子View具体layout的位置都是相对于父容器而言的,View的layout过程和measure同理,也是从顶级View开始,递归的完成整个空间树的布局操作。



经过前面的测量,控件树中的控件对于自己的尺寸显然已经了然于胸。而且父控件对于子控件的位置也有了眉目,所以经过测量过程后,布局阶段会把测量结果转化为控件的实际位置与尺寸。控件的实际位置与尺寸由View的mLeft,mTop,mRight,mBottom 等4个成员变量存储的坐标值来表示。


并且需要注意的是: View的mLeft,mTop,mRight,mBottom 这些坐标值是以父控件左上角为坐标原点进行计算的。倘若需要获取控件在窗口坐标系中的位置可以使用View.GetLocationWindow()或者是View.getRawX()/Y()。



//ViewRootImplprivate void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
//ViewGroup//尽管ViewGroup也重写了layout方法
//但是本质上还是会通过super.layout()调用View的layout()方法
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
//如果无动画,或者动画未运行
super.layout(l, t, r, b);
} else {
//等待动画完成时再调用requestLayout()
mLayoutCalledWhileSuppressed = true;
}
}//Viewpublic void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//如果布局有变化,通过setFrame重新布局
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//如果这是一个ViewGroup,还会遍历子View的layout()方法
//如果是普通View,通知具体实现类布局变更通知
onLayout(changed, l, t, r, b); //清除PFLAG_LAYOUT_REQUIRED标记
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
``````
//布局监听通知
}
//清除PFLAG_FORCE_LAYOUT标记
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
}

通过代码可以看到尽管ViewGroup也重写了layout()方法,但是本质上还是会走View的layout()。


在View的layout()方法里,首先通过setFrame()(setOpticalFrame()也走setFrame())将l、t、r、b分别设置到mLeft、mTop、mRight、和mBottom,这样就可以确定子View在父容器的位置了,上面也说过了,这些位置是相对父容器的。


然后调用onLayout()方法,使具体实现类接收到布局变更通知。如果此类是ViewGroup,还会遍历子View的layout()方法使其更新布局。如果调用的是onLayout()方法,这会导致子View无法调用setFrame(),从而无法更新控件坐标信息。


//View
protected void onLayout(boolean changed, int l, int t, int r, int b) {}//ViewGroup
//abstract修饰,具体实现类必须重写该方法
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

对于普通View来说,onLayout()方法是一个空实现,主要是具体实现类重写该方法后能够接收到布局坐标更新信息。


对于ViewGroup来说,和measure一样,不同实现类有它不同的布局特性,在ViewGroup中onLayout()方法是abstract的,具体实现类必须重写该方法,以便接收布局坐标更新信息后,处理自己的子View的坐标信息。有兴趣的童鞋可以看FrameLayout或者LinearLayout的onLayout()方法。

小结

对比测量measure和布局layout两个过程有助于加深对它们的理解。(摘自《深入理解Android卷III》)

measure确定的是控件的尺寸,并在一定程度上确定了子控件的位置。而布局则是针对测量结果来实施,并最终确定子控件的位置。


measure结果对布局过程没有约束力。虽说子控件在onMeasure()方法中计算出了自己应有的尺寸,但是由于layout()方法是由父控件调用,因此控件的位置尺寸的最终决定权掌握在父控件手中,测量结果仅仅只是一个参考。


因为measure过程是后根遍历(DecorView最后setMeasureDiemension()),所以子控件的测量结果影响父控件的测量结果。


而Layout过程是先根遍历(layout()一开始就调用setFrame()完成DecorView的布局),所以父控件的布局结果会影响子控件的布局结果。


完成performLayout()后,空间树的所有控件都已经确定了其最终位置,就剩下绘制了。


draw

我们先纯粹的看View的draw过程,因为这个过程相对上面measure和layout比较简单。


View的draw过程遵循如下几步 :

绘制背景drawBackground();


绘制自己onDraw();


如果是ViewGroup则绘制子View,dispatchDraw();


绘制装饰(滚动条)和前景,onDrawForeground();

//Viewpublic void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
//检查是否是"实心(不透明)"控件。(后面有补充)
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
*1. Draw the background
*2. If necessary, save the canvas' layers to prepare for fading
*3. Draw view's content
*4. Draw children
*5. If necessary, draw the fading edges and restore layers
*6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
//非"实心"控件,将会绘制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
//如果控件不需要绘制渐变边界,则可以进入简便绘制流程
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);//非"实心",则绘制控件本身
// Step 4, draw the children
dispatchDraw(canvas);//如果当前不是ViewGroup,此方法则是空实现
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);//绘制装饰和前景
// we're done...
return;
}
``````
}

这里写图片描述


至此View的工作流程的大致整体已经描述完毕了,是否感觉意犹未尽,我们再补充2个知识点作为餐后甜点。


invalidate

我们知道invalidate()(在主线程)和postInvalidate()(可以在子线程)都是用于请求View重绘的方法,那么它是如何实现的呢?


invalidate()方法必须在主线程执行,而scheduleTraversals()引发的遍历也是在主线程执行。所以调用invalidate()方法并不会使得遍历立即开始,因为在调用invalidate()的方法执行完毕之前(准确的说是主线程的Looper处理完其他消息之前),主线程根本没有机会处理scheduleTraversals()所发出的消息。


这种机制带来的好处是 : 在一个方法里可以连续调用多个控件的invalidate()方法,而不用担心会由于多次重绘而产生的效率问题。


另外多次调用invalidate()方法会使得ViewRootImpl多次接收到设置脏区域的请求,ViewRootImpl会将这些脏区域累加到mDirty中,进而在随后的遍历中,一次性的完成所有脏区域的重绘。


窗口第一次绘制时候,ViewRootImpl的mFullRedrawNeeded成员将会被设置为true,也就是说mDirty所描述的区域将会扩大到整个窗口,进而实现完整重绘。


View的脏区域和”实心”控件

增加两个知识点,能够更好的理解View的重绘过程。



为了保证绘制的效率,控件树仅对需要重绘的区域进行绘制。这部分区域成为”脏区域”Dirty Area。


当一个控件的内容发生变化而需要重绘时,它会通过View.invalidate()方法将其需要重绘的区域沿着控件树自下而上的交给ViewRootImpl,并保存在ViewRootImpl的mDirty成员中,最后通过scheduleTraversals()引发一次遍历,进而进行重绘工作,这样就可以保证仅位于mDirty所描述的区域得到重绘,避免了不必要的开销。


View的isOpaque()方法返回值表示此控件是否为”实心”的,所谓”实心”控件,是指在onDraw()方法中能够保证此控件的所有区域都会被其所绘制的内容完全覆盖。对于”实心”控件来说,背景和子元素(如果有的话)是被其onDraw()的内容完全遮住的,因此便可跳过遮挡内容的绘制工作从而提升效率。


简单来说透过此控件所属的区域无法看到此控件下的内容,也就是既没有半透明也没有空缺的部分。因为自定义ViewGroup控件默认是”实心”控件,所以默认不会调用drawBackground()和onDraw()方法,因为一旦ViewGroup的onDraw()方法,那么就会覆盖住它的子元素。但是我们仍然可以通过调用setWillNotDraw(false)和setBackground()方法来开启ViewGroup的onDraw()功能。



下面我们从View的invalidate方法,自下(View)而上(ViewRootImpl)的分析。


invalidate: 使无效;damage: 损毁;dirty: 脏;


View
//Viewpublic void invalidate() {
invalidate(true);
}
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
//如果VIew不可见,或者在动画中
if (skipInvalidate()) {
return;
}
//根据mPrivateFlags来标记是否重绘
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {//上面传入为true,表示需要全部重绘
mLastIsOpaque = isOpaque();//
mPrivateFlags &= ~PFLAG_DRAWN;//去除绘制完毕标记。
}
//添加标记,表示View正在绘制。PFLAG_DRAWN为绘制完毕。
mPrivateFlags |= PFLAG_DIRTY;
//清除缓存,表示由当前View发起的重绘。
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
//把需要重绘的区域传递给父View
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//设置重绘区域(区域为当前View在父容器中的整个布局)
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
``````
}
}

上述代码中,会设置一系列的标记位到mPrivateFlags中,并且通过父容器的invalidateChild方法,将需要重绘的脏区域传给父容器。(ViewGroup和ViewRootImpl都继承了ViewParent类,该类中定义了子元素与父容器间的调用规范。)


ViewGroup
//ViewGroup@Override
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
``````
//父容器根据自身对子View的脏区域进行调整
transformMatrix.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
// 这里的do while方法,不断的去调用父类的invalidateChildInParent方法来传递重绘请求
//直到调用到ViewRootImpl的invalidateChildInParent(责任链模式)
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
//如果父类是"实心"的,那么设置它的mPrivateFlags标识
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
//***往上递归调用父类的invalidateChildInParent***
parent = parent.invalidateChildInParent(location, dirty);
//设置父类的脏区域
//父容器会把子View的脏区域转化为父容器中的坐标区域
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
}
while (parent != null);
}
}
ViewRootImpl

我们先验证一下最上层ViewParent为什么是ViewRootImpl


//ViewRootImplpublic void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
view.assignParent(this);
}//Viewvoid assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}

在ViewRootImpl的setView方法中,由于传入的View正是DecorView,所以最顶层的ViewParent即ViewRootImpl。另外ViewGroup在addView方法中,也会调用assignParent()方法,设定子元素的父容器为它本身。


由于最上层的ViewParent是ViewRootImpl,所以我们可以查看ViewRootImpl的invalidateChildInParent方法即可。


//ViewRootImpl@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
//检查线程,这也是为什么invalidate一定要在主线程的原因
checkThread();
if (dirty == null) {
invalidate();//有可能需要绘制整个窗口
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
``````
invalidateRectOnScreen(dirty);
return null;
}//设置mDirty并执行View的工作流程
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
mAttachInfo.mSetIgnoreDirtyState = true;
mAttachInfo.mIgnoreDirtyState = true;
}
// Add the new dirty rect to the current one
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
//在这里,mDirty的区域就变为方法中的dirty,即要重绘的脏区域
``````
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();//执行View的工作流程
}
}

什么?执行invalidate()方法居然会引起scheduleTraversals()! 那么也就是说invalidate()会导致perforMeasure()、performLayout()、perforDraw()的调用了???


这个scheduleTraversals()很眼熟,我们一出场就在requestLayout()中见过,并且我们还说了mLayoutRequested用来表示是否measure和layout。


//ViewRootImpl@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检查是否在主线程
mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
scheduleTraversals();
}
}private void performTraversals() {
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
measureHierarchy(```);//measure
} final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);//layout
} boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
performDraw();//draw
}
}

因为我们invalidate的时候,并没有设置mLayoutRequested,所以放心,它只走performDraw()流程,并且在draw()流程中会清除mDirty区域。


并且只有设置了标识为的View才会调用draw方法进而调用onDraw(),减少开销。「源码工程师各方面的考虑肯定比一般人更周到,我们写的是代码,他们写的是艺术。」


Idtk--View的invalidate 查看大图(Idtk–invalidate文章所绘图片)


requestLayout

看完了invalidate()流程之后,requestLayout()流程就比较好上手了。


我们在measure阶段提到过 :



在view.measure()的方法里,仅当给与的MeasureSpec发生变化时,或要求强制重新布局时,才会进行测量。


强制重新布局 : 控件树中的一个子控件内容发生变化时,需要重新测量和布局的情况,在这种情况下,这个子控件的父控件(以及父控件的父控件)所提供的MeasureSpec必定与上次测量时的值相同,因而导致从ViewRootImpl到这个控件的路径上,父控件的measure()方法无法得到执行,进而导致子控件无法重新测量其布局和尺寸。(在父容器measure中遍历子元素)


解决途径 : 因此,当子控件因内容发生变化时,从子控件到父控件回溯到ViewRootImpl,并依次调用父控件的requestLayout()方法。这个方法会在mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利执行,进而这个子控件有机会进行重新布局与测量。这便是强制重新布局的意义所在。


下面我们看View的requestLayout()方法


//View
@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
``````
// 增加PFLAG_FORCE_LAYOUT标记,在measure时会校验此属性
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 父类不为空&&父类没有请求重新布局(是否有PFLAG_FORCE_LAYOUT标志)
//这样同一个父容器的多个子View同时调用requestLayout()就不会增加开销
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}

因为上面说过了,最顶层的ViewParent是ViewRootImpl。


//ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

同样,requestLayout()方法会调用scheduleTraversals();,因为设置了mLayoutRequested =true标识,所以在performTraversals()中调用performMeasure(),performLayout(),但是由于没有设置mDirty,所以不会走performDraw()流程。


但是,requestLayout()方法就一定不会导致onDraw()的调用吗?


在上面layout()方法中说道 :



在View的layout()方法里,首先通过setFrame()(setOpticalFrame()也走setFrame())将l、t、r、b分别设置到mLeft、mTop、mRight、和mBottom,这样就可以确定子View在父容器的位置了,上面也说过了,这些位置是相对父容器的。



//View -->layout()protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false; if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
//布局坐标改变了
changed = true;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);//调用invalidate重新绘制视图 if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
``````
}
return changed;
}

看完代码我们就很清晰的知道,如果layout布局有变化,那么它也会调用invalidate()重绘自身。


下面再借用Idtk绘制的layout流程图 Idtk--View的requestLayout


总结

至此,View的工作流程分析完毕,文章如果有错误或者不妥之处,还望评论提出。


理清整体流程对我们android的布局,绘制,自定义View,和分析bug都有一个提升。


有兴趣的还可以继续观看Android源码分析


Android View的工作流程


Android Window 机制探索


Android Activity 的启动过程


Android 消息机制——你真的了解Handler?


Android Binder之应用层总结与分析


参考

《Android开发艺术探索》 《深入理解Android》卷III 深入理解Android之View的绘制流程 Android View的绘制流程


Idtk 的博客

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台