当前位置:   article > 正文

Android应用层View绘制流程与源码分析(1)

Android应用层View绘制流程与源码分析(1)

还记得前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章3-3小节探讨的inflate方法加载一些布局显示时指定的大小失效问题吗?当时只给出了结论,现在给出了详细原因分析,我想不需要再做过多解释了吧。

至此整个View绘制流程的第一步就分析完成了,可以看见,相对来说还是比较复杂的,接下来进行小结。

2-2 measure原理总结

通过上面分析可以看出measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。具体measure核心主要有如下几点:

  • MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;

MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;

MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;

  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

  • View的布局大小由父View和子View共同决定。

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

3 View绘制流程第二步:递归layout源码分析


在上面的背景介绍就说过,当ViewRootImpl的performTraversals中measure执行完成以后会接着执行mView.layout,具体如下:

private void performTraversals() {

mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());

}

可以看见layout方法接收四个参数,这四个参数分别代表相对Parent的左、上、右、下坐标。而且还可以看见左上都为0,右下分别为上面刚刚测量的width和height。

至此又回归到View的layout(int l, int t, int r, int b)方法中去实现具体逻辑了,所以接下来我们开始分析View的layout过程。

整个View树的layout递归流程图如下:

这里写图片描述

3-1 layout源码分析

layout既然也是递归结构,那我们先看下ViewGroup的layout方法,如下:

@Override

public final void layout(int l, int t, int r, int b) {

super.layout(l, t, r, b);

}

看着没有?ViewGroup的layout方法实质还是调运了View父类的layout方法,所以我们看下View的layout源码,如下:

public void layout(int l, int t, int r, int b) {

//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量

//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout

boolean changed = isLayoutModeOptical(mParent) ?

setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

//需要重新layout

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

//回调onLayout

onLayout(changed, l, t, r, b);

}

}

看见没有,类似measure过程,lauout调运了onLayout方法。

对比上面View的layout和ViewGroup的layout方法可以发现,View的layout方法是可以在子类重写的,而ViewGroup的layout是不能在子类重写的,言外之意就是说ViewGroup中只能通过重写onLayout方法。那我们接下来看下ViewGroup的onLayout方法,如下:

@Override

protected abstract void onLayout(boolean changed,

int l, int t, int r, int b);

看见没有?ViewGroup的onLayout()方法竟然是一个抽象方法,这就是说所有ViewGroup的子类都必须重写这个方法。所以在自定义ViewGroup控件中,onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。

再看下View的onLayout方法源码,如下:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}

我勒个去!是一个空方法,没啥可看的。

既然这样那我们只能分析一个现有的继承ViewGroup的控件了,就拿LinearLayout来说吧,如下是LinearLayout中onLayout的一些代码:

public class LinearLayout extends ViewGroup {

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

if (mOrientation == VERTICAL) {

layoutVertical(l, t, r, b);

} else {

layoutHorizontal(l, t, r, b);

}

}

}

看见没有,LinearLayout的layout过程是分Vertical和Horizontal的,这个就是xml布局的orientation属性设置的,我们为例说明ViewGroup的onLayout重写一般步骤就拿这里的VERTICAL模式来解释吧,如下是layoutVertical方法源码:

void layoutVertical(int left, int top, int right, int bottom) {

final int paddingLeft = mPaddingLeft;

int childTop;

int childLeft;

// Where right end of child should go

//计算父窗口推荐的子View宽度

final int width = right - left;

//计算父窗口推荐的子View右侧位置

int childRight = width - mPaddingRight;

// Space available for child

//child可使用空间大小

int childSpace = width - paddingLeft - mPaddingRight;

//通过ViewGroup的getChildCount方法获取ViewGroup的子View个数

final int count = getVirtualChildCount();

//获取Gravity属性设置

final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;

final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

//依据majorGravity计算childTop的位置值

switch (majorGravity) {

case Gravity.BOTTOM:

// mTotalLength contains the padding already

childTop = mPaddingTop + bottom - top - mTotalLength;

break;

// mTotalLength contains the padding already

case Gravity.CENTER_VERTICAL:

childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;

break;

case Gravity.TOP:

default:

childTop = mPaddingTop;

break;

}

//重点!!!开始遍历

for (int i = 0; i < count; i++) {

final View child = getVirtualChildAt(i);

if (child == null) {

childTop += measureNullChild(i);

} else if (child.getVisibility() != GONE) {

//LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值

final int childWidth = child.getMeasuredWidth();

final int childHeight = child.getMeasuredHeight();

//获取子View的LayoutParams

final LinearLayout.LayoutParams lp =

(LinearLayout.LayoutParams) child.getLayoutParams();

int gravity = lp.gravity;

if (gravity < 0) {

gravity = minorGravity;

}

final int layoutDirection = getLayoutDirection();

final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);

//依据不同的absoluteGravity计算childLeft位置

switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {

case Gravity.CENTER_HORIZONTAL:

childLeft = paddingLeft + ((childSpace - childWidth) / 2)

  • lp.leftMargin - lp.rightMargin;

break;

case Gravity.RIGHT:

childLeft = childRight - childWidth - lp.rightMargin;

break;

case Gravity.LEFT:

default:

childLeft = paddingLeft + lp.leftMargin;

break;

}

if (hasDividerBeforeChildAt(i)) {

childTop += mDividerHeight;

}

childTop += lp.topMargin;

//通过垂直排列计算调运child的layout设置child的位置

setChildFrame(child, childLeft, childTop + getLocationOffset(child),

childWidth, childHeight);

childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);

}

}

}

从上面分析的ViewGroup子类LinearLayout的onLayout实现代码可以看出,一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子View的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子View的具体位置。

到这里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别(上面分析measure过程已经说过getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效)。可以看出来getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。那我们看下View源码中这些方法的实现吧,如下:

public final int getMeasuredWidth() {

return mMeasuredWidth & MEASURED_SIZE_MASK;

}

public final int getMeasuredHeight() {

return mMeasuredHeight & MEASURED_SIZE_MASK;

}

public final int getWidth() {

return mRight - mLeft;

}

public final int getHeight() {

return mBottom - mTop;

}

public final int getLeft() {

return mLeft;

}

public final int getRight() {

return mRight;

}

public final int getTop() {

return mTop;

}

public final int getBottom() {

return mBottom;

}

这也解释了为什么有些情况下getWidth()和getMeasuredWidth()以及getHeight()和getMeasuredHeight()会得到不同的值,所以这里不做过多解释。

到此整个View的layout过程分析就算结束了,接下来进行一些总结工作。

3-2 layout原理总结

整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

  • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

  • 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的(前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》也有提到过)。

  • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

4 View绘制流程第三步:递归draw源码分析


在上面的背景介绍就说过,当ViewRootImpl的performTraversals中measure和layout执行完成以后会接着执行mView.layout,具体如下:

private void performTraversals() {

final Rect dirty = mDirty;

canvas = mSurface.lockCanvas(dirty);

mView.draw(canvas);

}

draw过程也是在ViewRootImpl的performTraversals()内部调运的,其调用顺序在measure()和layout()之后,这里的mView对于Actiity来说就是PhoneWindow.DecorView,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程。

先来看下View树的递归draw流程图,如下:

这里写图片描述

如下我们详细分析这一过程。

4-1 draw源码分析

由于ViewGroup没有重写View的draw方法,所以如下直接从View的draw方法开始分析:

public void draw(Canvas canvas) {

/*

  • Draw traversal performs several drawing steps which must be executed

  • in the appropriate order:

  •  1. Draw the background
    
    • 1
  •  2. If necessary, save the canvas' layers to prepare for fading
    
    • 1
  •  3. Draw view's content
    
    • 1
  •  4. Draw children
    
    • 1
  •  5. If necessary, draw the fading edges and restore layers
    
    • 1
  •  6. Draw decorations (scrollbars for instance)
    
    • 1

*/

// Step 1, draw the background, if needed

if (!dirtyOpaque) {

drawBackground(canvas);

}

// skip step 2 & 5 if possible (common case)

// Step 2, save the canvas’ layers

if (drawTop) {

canvas.saveLayer(left, top, right, top + length, null, flags);

}

// Step 3, draw the content

if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

// Step 5, draw the fade effect and restore layers

if (drawTop) {

matrix.setScale(1, fadeHeight * topFadeStrength);

matrix.postTranslate(left, top);

fade.setLocalMatrix(matrix);

p.setShader(fade);

canvas.drawRect(left, top, right, top + length, p);

}

// Step 6, draw decorations (scrollbars)

onDrawScrollBars(canvas);

}

看见整个View的draw方法很复杂,但是源码注释也很明显。从注释可以看出整个draw过程分为了6步。源码注释说(”skip step 2 & 5 if possible (common case)”)第2和5步可以跳过,所以我们接下来重点剩余四步。如下:

第一步,对View的背景进行绘制。

可以看见,draw方法通过调运drawBackground(canvas);方法实现了背景绘制。我们来看下这个方法源码,如下:

private void drawBackground(Canvas canvas) {

//获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable

final Drawable background = mBackground;

//根据layout过程确定的View位置来设置背景的绘制区域

if (mBackgroundSizeChanged) {

background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);

mBackgroundSizeChanged = false;

rebuildOutline();

}

//调用Drawable的draw()方法来完成背景的绘制工作

background.draw(canvas);

}

第三步,对View的内容进行绘制。

可以看到,这里去调用了一下View的onDraw()方法,所以我们看下View的onDraw方法(ViewGroup也没有重写该方法),如下:

/**

  • Implement this to do your drawing.

  • @param canvas the canvas on which the background will be drawn

*/

protected void onDraw(Canvas canvas) {

}

可以看见,这是一个空方法。因为每个View的内容部分是各不相同的,所以需要由子类去实现具体逻辑。

第四步,对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制。

我们来看下View的draw方法中的dispatchDraw(canvas);方法源码,可以看见如下:

/**

  • Called by draw to draw the child views. This may be overridden

  • by derived classes to gain control just before its children are drawn

  • (but after its own view has been drawn).

  • @param canvas the canvas on which to draw the view

*/

protected void dispatchDraw(Canvas canvas) {

}

看见没有,View的dispatchDraw()方法是一个空方法,而且注释说明了如果View包含子类需要重写他,所以我们有必要看下ViewGroup的dispatchDraw方法源码(这也就是刚刚说的对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制的原因,因为如果是View调运该方法是空的,而ViewGroup才有实现),如下:

@Override

protected void dispatchDraw(Canvas canvas) {

final int childrenCount = mChildrenCount;

final View[] children = mChildren;

for (int i = 0; i < childrenCount; i++) {

if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {

more |= drawChild(canvas, child, drawingTime);

}

}

// Draw any disappearing views that have animations

if (mDisappearingChildren != null) {

for (int i = disappearingCount; i >= 0; i–) {

more |= drawChild(canvas, child, drawingTime);

}

}

}

可以看见,ViewGroup确实重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

return child.draw(canvas, this, drawingTime);

}

可以看见drawChild()方法调运了子View的draw()方法。所以说ViewGroup类已经为我们重写了dispatchDraw()的功能实现,我们一般不需要重写该方法,但可以重载父类函数实现具体的功能。

第六步,对View的滚动条进行绘制。

可以看到,这里去调用了一下View的onDrawScrollBars()方法,所以我们看下View的onDrawScrollBars(canvas);方法,如下:

/**

  • Request the drawing of the horizontal and the vertical scrollbar. The

  • scrollbars are painted only if they have been awakened first.

  • @param canvas the canvas on which to draw the scrollbars

  • @see #awakenScrollBars(int)

*/

protected final void onDrawScrollBars(Canvas canvas) {

//绘制ScrollBars分析不是我们这篇的重点,所以暂时不做分析

}

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。

到此,View的draw绘制部分源码分析完毕,我们接下来进行一些总结。

4-2 draw原理总结

可以看见,绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:

  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

  • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

  • View的绘制是借助onDraw方法传入的Canvas类来进行的。

  • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

5 View的invalidate和postInvalidate方法源码分析


你可能已经看见了,在上面分析View的三步绘制流程中最后都有调运一个叫invalidate的方法,这个方法是啥玩意?为何出现频率这么高?很简单,我们拿出来分析分析不就得了。

5-1 invalidate方法源码分析

来看一下View类中的一些invalidate方法(ViewGroup没有重写这些方法),如下:

/**

  • Mark the area defined by dirty as needing to be drawn. If the view is

  • visible, {@link #onDraw(android.graphics.Canvas)} will be called at some

  • point in the future.

  • This must be called from a UI thread. To call from a non-UI thread, call

  • {@link #postInvalidate()}.

  • WARNING: In API 19 and below, this method may be destructive to

  • {@code dirty}.

  • @param dirty the rectangle representing the bounds of the dirty region

*/

//看见上面注释没有?public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View

public void invalidate(Rect dirty) {

final int scrollX = mScrollX;

final int scrollY = mScrollY;

//实质还是调运invalidateInternal方法

invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,

dirty.right - scrollX, dirty.bottom - scrollY, true, false);

}

/**

  • Mark the area defined by the rect (l,t,r,b) as needing to be drawn. The

  • coordinates of the dirty rect are relative to the view. If the view is

  • visible, {@link #onDraw(android.graphics.Canvas)} will be called at some

  • point in the future.

  • This must be called from a UI thread. To call from a non-UI thread, call

  • {@link #postInvalidate()}.

  • @param l the left position of the dirty region

  • @param t the top position of the dirty region

  • @param r the right position of the dirty region

  • @param b the bottom position of the dirty region

*/

//看见上面注释没有?public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View

public void invalidate(int l, int t, int r, int b) {

final int scrollX = mScrollX;

final int scrollY = mScrollY;

//实质还是调运invalidateInternal方法

invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);

}

/**

  • Invalidate the whole view. If the view is visible,

  • {@link #onDraw(android.graphics.Canvas)} will be called at some point in

  • the future.

  • This must be called from a UI thread. To call from a non-UI thread, call

  • {@link #postInvalidate()}.

*/

//看见上面注释没有?public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View

public void invalidate() {

//invalidate的实质还是调运invalidateInternal方法

invalidate(true);

}

/**

  • This is where the invalidate() work actually happens. A full invalidate()

  • causes the drawing cache to be invalidated, but this function can be

  • called with invalidateCache set to false to skip that invalidation step

  • for cases that do not need it (for example, a component that remains at

  • the same dimensions with the same content).

  • @param invalidateCache Whether the drawing cache for this view should be

  •        invalidated as well. This is usually true for a full
    
    • 1
  •        invalidate, but may be set to false if the View's contents or
    
    • 1
  •        dimensions have not changed.
    
    • 1

*/

//看见上面注释没有?default的权限,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View

void invalidate(boolean invalidateCache) {

//实质还是调运invalidateInternal方法

invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);

}

//!!!!!!看见没有,这是所有invalidate的终极调运方法!!!!!!

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,

boolean fullInvalidate) {

// Propagate the damage rectangle to the parent view.

final AttachInfo ai = mAttachInfo;

final ViewParent p = mParent;

if (p != null && ai != null && l < r && t < b) {

final Rect damage = ai.mTmpInvalRect;

//设置刷新区域

damage.set(l, t, r, b);

//传递调运Parent ViewGroup的invalidateChild方法

p.invalidateChild(this, damage);

}

}

看见没有,View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。所以我们看下ViewGroup的invalidateChild方法,源码如下:

public final void invalidateChild(View child, final Rect dirty) {

ViewParent parent = this;

final AttachInfo attachInfo = mAttachInfo;

do {

//循环层层上级调运,直到ViewRootImpl会返回null

parent = parent.invalidateChildInParent(location, dirty);

} while (parent != null);

}

这个过程最后传递到ViewRootImpl的invalidateChildInParent方法结束,所以我们看下ViewRootImpl的invalidateChildInParent方法,如下:

@Override

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

//View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法

scheduleTraversals();

return null;

}

看见没有?这个ViewRootImpl类的invalidateChildInParent方法直接返回了null,也就是上面ViewGroup中说的,层层上级传递到ViewRootImpl的invalidateChildInParent方法结束了那个do while循环。看见这里调运的scheduleTraversals这个方法吗?scheduleTraversals会通过Handler的Runnable发送一个异步消息,调运doTraversal方法,然后最终调用performTraversals()执行重绘。开头背景知识介绍说过的,performTraversals就是整个View数开始绘制的起始调运地方,所以说View调运invalidate方法的实质是层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals方法,然后整个View树开始重新按照上面分析的View绘制流程进行重绘任务。

到此View的invalidate方法原理就分析完成了。

5-2 postInvalidate方法源码分析

上面分析invalidate方法时注释中说该方法只能在UI Thread中执行,其他线程中需要使用postInvalidate方法,所以我们来分析分析postInvalidate这个方法源码。如下:

public void postInvalidate() {

postInvalidateDelayed(0);

}

继续看下他的调运方法postInvalidateDelayed,如下:

public void postInvalidateDelayed(long delayMilliseconds) {

// We try only with the AttachInfo because there’s no point in invalidating

// if we are not attached to our window

final AttachInfo attachInfo = mAttachInfo;

//核心,实质就是调运了ViewRootImpl.dispatchInvalidateDelayed方法

if (attachInfo != null) {

attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);

}

}

我们继续看他调运的ViewRootImpl类的dispatchInvalidateDelayed方法,如下源码:

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {

Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);

mHandler.sendMessageDelayed(msg, delayMilliseconds);

}

看见没有,通过ViewRootImpl类的Handler发送了一条MSG_INVALIDATE消息,继续追踪这条消息的处理可以发现:

public void handleMessage(Message msg) {

switch (msg.what) {

case MSG_INVALIDATE:

((View) msg.obj).invalidate();

break;

}

}

看见没有,实质就是又在UI Thread中调运了View的invalidate();方法,那接下来View的invalidate();方法我们就不说了,上名已经分析过了。

到此整个View的postInvalidate方法就分析完成了。

5-3 invalidate与postInvalidate方法总结

依据上面对View的invalidate分析我总结绘制如下流程图:

这里写图片描述

依据上面对View的postInvalidate分析我总结绘制如下流程图:

这里写图片描述

关于这两个方法的具体流程和原理上面也分析过了,流程图也给出了,相信已经很明确了,没啥需要解释的了。所以我们对其做一个整体总结,归纳出重点如下:

invalidate系列方法请求重绘View树(也就是draw方法),如果View大小没有发生变化就不会调用layout过程,并且只绘制那些“需要重绘的”View,也就是哪个View(View只绘制该View,ViewGroup绘制整个ViewGroup)请求invalidate系列方法,就绘制该View。

常见的引起invalidate方法操作的原因主要有:

  • 直接调用invalidate方法.请求重新draw,但只会绘制调用者本身。

  • 触发setSelection方法。请求重新draw,但只会绘制调用者本身。

  • 触发setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及draw过程,同样只绘制需要“重新绘制”的视图。

  • 触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。

  • 触发requestFocus方法。请求View树的draw过程,只绘制“需要重绘”的View。

5-4 通过invalidate方法分析结果回过头去解决一个背景介绍中的疑惑

分析完invalidate后需要你回过头去想一个问题。还记不记得这篇文章的开头背景介绍,我们说整个View绘制流程的最初代码是在ViewRootImpl类的performTraversals()方法中开始的。上面当时只是告诉你了这个结论,至于这个ViewRootImpl类的performTraversals()方法为何会被触发没有说明原因。现在我们就来分析一下这个触发的源头。

让我们先把大脑思考暂时挪回到《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇博文的setContentView机制分析中(不清楚的请点击先看这篇文章再回过头来继续看)。我们先来看下那篇博文分析的PhoneWindow的setContentView方法源码,如下:

@Override

public void setContentView(View view, ViewGroup.LayoutParams params) {

//如果mContentParent为空进行一些初始化,实质mContentParent是通过findViewById(ID_ANDROID_CONTENT);获取的id为content的FrameLayout的布局(不清楚的请先看《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章)

if (mContentParent == null) {

installDecor();

}


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

作为一名从事Android的开发者,很多人最近都在和我吐槽Android是不是快要凉了?而在我看来这正是市场成熟的表现,所有的市场都是温水煮青蛙,永远会淘汰掉不愿意学习改变,安于现状的那批人,希望所有的人能在大浪淘沙中留下来,因为对于市场的逐渐成熟,平凡并不是我们唯一的答案!

资料.png
资料图.jpg

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

ContentView方法源码,如下:

@Override

public void setContentView(View view, ViewGroup.LayoutParams params) {

//如果mContentParent为空进行一些初始化,实质mContentParent是通过findViewById(ID_ANDROID_CONTENT);获取的id为content的FrameLayout的布局(不清楚的请先看《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章)

if (mContentParent == null) {

installDecor();

}


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-Vacr4kAn-1712556437799)]

[外链图片转存中…(img-avXXE2Tq-1712556437800)]

[外链图片转存中…(img-AXw4oZOB-1712556437800)]

[外链图片转存中…(img-xV5YVJE6-1712556437800)]

[外链图片转存中…(img-3ruLnLZb-1712556437801)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

作为一名从事Android的开发者,很多人最近都在和我吐槽Android是不是快要凉了?而在我看来这正是市场成熟的表现,所有的市场都是温水煮青蛙,永远会淘汰掉不愿意学习改变,安于现状的那批人,希望所有的人能在大浪淘沙中留下来,因为对于市场的逐渐成熟,平凡并不是我们唯一的答案!

[外链图片转存中…(img-eFKgIdEd-1712556437801)]
[外链图片转存中…(img-w7sjUEKy-1712556437801)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/711322
推荐阅读
相关标签
  

闽ICP备14008679号