粗谈绘制任务和绘制流程

开发 项目管理
当有绘制任务的时候,会将这个任务交给Choreographer,然后再等下一个VSync信号来的时候,执行到ViewRootImpl的performTraversals方法。

[[395776]]

前言

今天是2028年4月26日,天气晴,我请了一天假在家陪女儿。

正在陪女儿画画的我,被女儿问到:

??:“爸爸,妈妈说你的工作是可以把我们想到的东西变到手机上,是这样吗?”

??:“对呀,厉害吧~”

??:“那你可以把我们家的小狗狗变到手机上吗?”

??:“当然可以了,不过手机是很笨的东西,必须我们把所有的规则写好,他才能听我们的话~”

??:“什么规则呀“

简述绘制流程

你看,手机屏幕只有这么大,所以我们先要确定狗狗的大小,该画多大的狗狗,可以画多大的狗狗。

这就是测量的过程。

接着,我们要确定狗狗放在哪里,左上角还是中间还是右下角?

这就是布局的过程。

最后,我们就要画出狗狗的样子,是斑点狗还是大狼狗,是小白狗还是小黑狗。

这就是绘画的过程。

所以,在手机上变出一只狗狗,或者变出任何一个东西都需要三个步骤:

  • 测量(measure)
  • 布局(layout)
  • 绘画(draw)

绘制任务的来源

把视线拉回到成年人的世界。

第一次界面绘制

上篇文章说到,当有绘制任务的时候,会将这个任务交给Choreographer,然后再等下一个VSync信号来的时候,执行到ViewRootImpl的performTraversals方法。

那么这个任务到底从何而来呢?回顾下Activity的显示过程:

  • 首先在setContentView方法中,创建了DecorView。
  • 然后在handleResumeActivity方法中,执行了addView方法将DecorView添加到WindowManager。
  • 最后设置DecorView对用户可见。

所以在第二步addView方法中,肯定进行了与View绘制有关的操作:

  1. //WindowManagerGlobal.java 
  2.  public void addView() { 
  3.         synchronized (mLock) { 
  4.             root = new ViewRootImpl(view.getContext(), display); 
  5.             view.setLayoutParams(wparams); 
  6.             mViews.add(view); 
  7.             mRoots.add(root); 
  8.             mParams.add(wparams); 
  9.             try { 
  10.                 root.setView(view, wparams, panelParentView); 
  11.             }  
  12.         } 
  13.     } 
  14.  
  15.     //ViewRootImpl.java 
  16.     public void setView() { 
  17.         synchronized (this) { 
  18.          //绘制 
  19.          requestLayout(); 
  20.          //调用WMS的addWindow方法 
  21.          res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, 
  22.                             getHostVisibility(), mDisplay.getDisplayId(), mWinFrame, 
  23.                             mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, 
  24.                             mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel); 
  25.          //设置this(ViewRootImpl)为view(decorView)的parent 
  26.    view.assignParent(this); 
  27.         } 
  28.     } 
  29.  
  30.  
  31.     //ViewRootImpl.java 
  32.     @Override 
  33.     public void requestLayout() { 
  34.         if (!mHandlingLayoutInLayoutRequest) { 
  35.             checkThread(); 
  36.             mLayoutRequested = true
  37.             scheduleTraversals(); 
  38.         } 
  39.     } 
  40.  
  41.     ->scheduleTraversals() 
  42.     ->performMeasure() performLayout() performDraw() 
  43.     ->measure、layout、draw方法 

在addView方法中,创建了ViewRootImpl,执行了setView方法,在这里调用了requestLayout方法开始了View的绘制工作。

所以这里就是Activity显示界面所做的第一次绘制来源。

那后续界面上的元素改变带来的绘制呢?

View.requestLayout

首先看看在View中调用requestLayout方法会怎么绘制,比如TextView.setText,最后就会执行到requestLayout

  1. //View.java 
  2. public void requestLayout() { 
  3.         
  4.  //设置两个标志位 
  5.        mPrivateFlags |= PFLAG_FORCE_LAYOUT; 
  6.        mPrivateFlags |= PFLAG_INVALIDATED; 
  7.  
  8.        //执行父view的requestLayout方法 
  9.        if (mParent != null && !mParent.isLayoutRequested()) { 
  10.            mParent.requestLayout(); 
  11.        } 
  12.    } 

精简之后的代码,主要干了两件事:

1、设置两个标志位,PFLAG_FORCE_LAYOUT 和 PFLAG_INVALIDATED。

2、执行父View的requestLayout方法。

这里的标志位暂且按下不表,待会就会遇到。从第二点可以看到View会一直向上执行requestLayout方法,而顶层的View就是DecorView,DecorView的parent就是ViewRootImpl。

所以最后还是执行到了ViewRootImpl的requestLayout方法,开始整个View树的 测量、布局、绘画。

  1. //ViewRootImpl.java 
  2.     @Override 
  3.     public void requestLayout() { 
  4.         if (!mHandlingLayoutInLayoutRequest) { 
  5.             checkThread(); 
  6.             mLayoutRequested = true
  7.             scheduleTraversals(); 
  8.         } 
  9.     } 

其中,mLayoutRequested字段设置为true,这是第二个标志位,待会也会遇到。

但是这有点奇怪哦?我一个View改变了,为什么整个界面的View树都需要重新绘制呢?

这是因为每个子View直接或多或少都会产生联系,比如一个RelativeLayout,一个View在TextView的右边,一个View在TextView的下面。

那么当TextView长度宽度变化了,那么其他的View自然也需要跟着变化,所以就必须整个View树进行重新绘制,保证布局的完整性。

View.invalidate/postInvalidate

还有一种触发绘制的情况就是View.invalidate/postInvalidate,postInvalidate一般用于子线程,最后也会调用到invalidate方法,就不单独说了。

invalidate方法一般用于View内部的重新绘画,比如同样是TextView.setText,也会触发invalidate方法。

  1. public void invalidate(boolean invalidateCache) { 
  2.         invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true); 
  3.     } 
  4.  
  5.  void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, 
  6.             boolean fullInvalidate) { 
  7.  
  8.         if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) 
  9.                 || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) 
  10.                 || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED 
  11.                 || (fullInvalidate && isOpaque() != mLastIsOpaque)) { 
  12.  
  13.             mPrivateFlags |= PFLAG_DIRTY; 
  14.  
  15.             final ViewParent p = mParent; 
  16.             if (p != null && ai != null && l < r && t < b) { 
  17.                 damage.set(l, t, r, b); 
  18.                 p.invalidateChild(this, damage); 
  19.             } 
  20.  
  21.         } 
  22.     } 

可以看到,这里调用了invalidateInternal方法,并且传入了可绘制的区域,最后调用了父view的invalidateChild方法。

  1. public final void invalidateChild(View child, final Rect dirty) { 
  2.         ViewParent parent = this; 
  3.         if (attachInfo != null) { 
  4.             do { 
  5.                 parent = parent.invalidateChildInParent(location, dirty); 
  6.             } while (parent != null); 
  7.         } 
  8.     } 

一个dowhile循环,不断调用父View的invalidateChildInParent方法。

也就是会执行ViewGroup的invalidateChildInParent,最后再执行ViewRootImpl的invalidateChildInParent方法,我们就直接看ViewRootImpl:

  1. //ViewRootImpl.java 
  2.  public ViewParent invalidateChildInParent(int[] location, Rect dirty) { 
  3.          
  4.         invalidateRectOnScreen(dirty); 
  5.         return null
  6.     } 
  7.  
  8.     private void invalidateRectOnScreen(Rect dirty) { 
  9.         if (!mWillDrawSoon && (intersected || mIsAnimating)) { 
  10.             scheduleTraversals(); 
  11.         } 
  12.     } 

完事,果不其然,又到了scheduleTraversals绘制方法。

(这其中还有很多关于Dirty区域的绘制和转换我省略了,Dirty区域就是需要重新绘图的区域)

那invalidate和requestLayout有什么区别呢?继续研究scheduleTraversals方法。

peformTraversals

接下来就看看peformTraversals方法是怎么触发到三大绘制流程的。

  1. private void performTraversals() { 
  2.  boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw); 
  3.  //测量 
  4.  if (layoutRequested) { 
  5.         windowSizeMayChange |= measureHierarchy(host, lp, res, 
  6.                     desiredWindowWidth, desiredWindowHeight); 
  7.     } 
  8.  
  9.     //布局 
  10.     final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); 
  11.     if (didLayout) { 
  12.         performLayout(lp, mWidth, mHeight); 
  13.     } 
  14.  
  15.     //绘画 
  16.     boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible; 
  17.     if (!cancelDraw) { 
  18.         performDraw(); 
  19.     } 

我只保留了与三大绘制流程相关的直接代码,可以看到:

1、测量过程的前提是layoutRequested为true,与mLayoutRequested有关。

2、布局过程的前提是didLayout,也与mLayoutRequested有关。

3、绘画过程的前提是!cancelDraw。

而mLayoutRequested字段是在requestlayout方法中进行设置的,invalidate方法中并没有设置。所以我们可以初步断定,只有requestLayout方法才会执行到onMeasure和onLayout。

测量(measureHierarchy)

  1. private boolean measureHierarchy() { 
  2.  
  3.         childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); 
  4.         childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 
  5.         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 
  6.  
  7.         return windowSizeMayChange; 
  8.     } 
  9.  
  10.  private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { 
  11.         try { 
  12.             mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
  13.         }  
  14.     } 
  15.  
  16.  public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 
  17.         final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; 
  18.         final boolean needsLayout = specChanged 
  19.                 && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize); 
  20.  
  21.         if (forceLayout || needsLayout) { 
  22.             // first clears the measured dimension flag 
  23.             onMeasure(widthMeasureSpec, heightMeasureSpec); 
  24.  
  25.             mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; 
  26.         } 
  27.  
  28.     } 

在measure方法中,我们判断了两个字段forceLayout和needsLayout,当其中有一个为true的时候,才会继续执行onMeasure。其中forceLayout字段代表的是mPrivateFlags标志位是不是PFLAG_FORCE_LAYOUT。

PFLAG_FORCE_LAYOUT?是不是有点熟悉。刚才在View.requestLayout方法中,就对每个View都设置了这个标志,所以才能触发到onMeasure进行测量。

所以requestLayout方法通过这个标志位 PFLAG_FORCE_LAYOUT,使每个子View都能进入到onMeasure流程。

布局(performLayout)

  1. private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, 
  2.            int desiredWindowHeight) { 
  3.        final View host = mView; 
  4.        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 
  5.    } 
  6.  
  7.    public void layout(int l, int t, int r, int b) { 
  8.   
  9.        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { 
  10.            onLayout(changed, l, t, r, b); 
  11.        } 
  12.        
  13.    } 

可以看到在layout方法中,是通过PFLAG_LAYOUT_REQUIRED标记来决定是否执行onLayout方法,而这个标记是在onMeasure方法执行之后设置的。

说明了只要onMeasure方法执行了,那么onLayout方法肯定也会执行,这两个方法是兄弟伙的关系,有你就有我。

绘画(performDraw)

  1. private void performDraw() { 
  2.        boolean canUseAsync = draw(fullRedrawNeeded); 
  3.    } 
  4.  
  5.    private boolean draw(boolean fullRedrawNeeded){ 
  6.     if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { 
  7.      if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, 
  8.                        scalingRequired, dirty, surfaceInsets)) { 
  9.                    return false
  10.          } 
  11.      } 
  12.         return useAsyncReport; 
  13.  
  14.    } 
  15.  
  16.    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, 
  17.            boolean scalingRequired, Rect dirty, Rect surfaceInsets) { 
  18.  
  19.        mView.draw(canvas); 
  20.        return true
  21.    } 
  22.  
  23.    public void draw(Canvas canvas) { 
  24.        final int privateFlags = mPrivateFlags; 
  25.        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; 
  26.  
  27.        /* 
  28.         * Draw traversal performs several drawing steps which must be executed 
  29.         * in the appropriate order
  30.         * 
  31.         *      1. Draw the background 
  32.         *      2. If necessary, save the canvas' layers to prepare for fading 
  33.         *      3. Draw view's content 
  34.         *      4. Draw children 
  35.         *      5. If necessary, draw the fading edges and restore layers 
  36.         *      6. Draw decorations (scrollbars for instance) 
  37.         */ 
  38.  
  39.        // Step 1, draw the background, if needed 
  40.        drawBackground(canvas); 
  41.  
  42.        // Step 2, save the canvas' layers 
  43.        canvas.saveUnclippedLayer.. 
  44.  
  45.        // Step 3, draw the content 
  46.        onDraw(canvas); 
  47.  
  48.        // Step 4, draw the children 
  49.        dispatchDraw(canvas); 
  50.  
  51.        // Step 5, draw the fade effect and restore layers 
  52.        canvas.drawRect.. 
  53.  
  54.        // Step 6, draw decorations (foreground, scrollbars) 
  55.        onDrawForeground(canvas); 
  56.  
  57.    } 

先看第二步draw(boolean fullRedrawNeeded)方法:

在该方法中,判断了dirty是否为空,只有不为空的话才会继续执行下去。dirty是什么?刚才也说过,就是需要重绘的区域。

而我们调用invalidate方法的目的就是向上传递dirty区域,最终生成屏幕上需要重绘的dirty,requestLayout方法中并没有对dirty区域进行设定。

继续看draw(Canvas canvas)方法,注释还是比较清晰的,一共分为了六步:

  • 1、绘制背景
  • 2、保存图层信息
  • 3、绘制内容(onDraw)
  • 4、绘制children
  • 5、绘制边缘
  • 6、绘制装饰

而我们常用的onDraw就是用于绘制内容。

总结

到此,View的绘制大体流程就结束了。

当然,其中还有大量细节,比如具体的绘制流程、需要注意的细节、自定义View实现等等,我们后面慢慢说道。

之前我们的问题,现在也可以解答了,就是绘制的两个请求:requestLayout和invalidate区别是什么?

  • requestLayout方法。会依次执行performMeasure、performLayout、performDraw,但在performDraw方法中由于没有dirty区域,一般情况下是不会执行onDraw。也有特殊情况,比如顶点发生变化。
  • invalidate方法。由于没有设置标示,只会走onDraw流程进行dirty区域重绘。

所以如果某个元素的改变涉及到宽高布局的改变,就需要执行requestLayout()。如果某个元素之需要内部区域进行重新绘制,就执行invalidate().

如果都需要,就先执行requestLayout(),在执行invalidate(),比如TextView.setText()。

参考

https://www.jianshu.com/p/e79a55c141d6

https://juejin.cn/post/6904518722564653070

本文转载自微信公众号「码上积木」,可以通过以下二维码关注。转载本文请联系码上积木公众号。

 

责任编辑:武晓燕 来源: 码上积木
相关推荐

2017-08-21 21:36:23

AndroidViewJava

2023-08-23 19:21:38

流程图时序图UML

2020-03-02 18:56:03

PythonGNU Octave编程语言

2013-05-23 14:50:55

2011-02-18 11:22:01

2010-06-09 19:25:54

UML活动图

2022-10-18 23:53:20

Python数据Matplotlib

2010-06-11 10:55:51

UML部署图

2010-06-09 08:59:30

UML活动图

2020-07-28 21:38:24

跨职能流程图

2013-04-15 14:23:21

2019-08-05 13:20:35

Android绘制代码

2020-07-16 08:33:38

ViewGroupView

2021-09-30 07:36:51

AndroidViewDraw

2011-08-17 14:32:44

iOS开发绘制

2022-08-29 14:22:03

bpmn.jsVue流程

2015-07-22 10:57:36

watchOS图表自定义

2022-07-15 16:04:22

R 语言

2022-06-06 10:44:10

C++语言鸿蒙

2023-05-08 09:08:33

CSS前端
点赞
收藏

51CTO技术栈公众号