专栏首页码上积木粗谈绘制任务和绘制流程

粗谈绘制任务和绘制流程

前言

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

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

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

?:“对呀,厉害吧~”

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

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

?:“什么规则呀“

简述绘制流程

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

这就是测量的过程。

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

这就是布局的过程。

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

这就是绘画的过程。

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

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

绘制任务的来源

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

第一次界面绘制

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

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

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

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

//WindowManagerGlobal.java
 public void addView() {
        synchronized (mLock) {
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            try {
                root.setView(view, wparams, panelParentView);
            } 
        }
    }

    //ViewRootImpl.java
    public void setView() {
        synchronized (this) {
         //绘制
         requestLayout();
         //调用WMS的addWindow方法
         res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
         //设置this(ViewRootImpl)为view(decorView)的parent
   view.assignParent(this);
        }
    }


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

    ->scheduleTraversals()
    ->performMeasure() performLayout() performDraw()
    ->measure、layout、draw方法

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

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

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

View.requestLayout

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

 //View.java
 public void requestLayout() {
        
  //设置两个标志位
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        //执行父view的requestLayout方法
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
    }

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

  • 1、设置两个标志位,PFLAG_FORCE_LAYOUTPFLAG_INVALIDATED
  • 2、执行父View的requestLayout方法。

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

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

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

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

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

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

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

View.invalidate/postInvalidate

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

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

    public 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) {

        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)) {

            mPrivateFlags |= PFLAG_DIRTY;

            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

        }
    }

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

public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;
        if (attachInfo != null) {
            do {
                parent = parent.invalidateChildInParent(location, dirty);
            } while (parent != null);
        }
    }

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

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

//ViewRootImpl.java
 public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        
        invalidateRectOnScreen(dirty);
        return null;
    }

    private void invalidateRectOnScreen(Rect dirty) {
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    }

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

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

invalidaterequestLayout有什么区别呢?继续研究scheduleTraversals方法。

peformTraversals

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

private void performTraversals() {
 boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
 //测量
 if (layoutRequested) {
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
    }

    //布局
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
    }

    //绘画
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw) {
        performDraw();
    }
}

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

  • 1、测量过程的前提是layoutRequested为true,与mLayoutRequested有关。
  • 2、布局过程的前提是didLayout,也与mLayoutRequested有关。
  • 3、绘画过程的前提是!cancelDraw

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

测量(measureHierarchy)

 private boolean measureHierarchy() {

        childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

        return windowSizeMayChange;
    }

 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } 
    }

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

    }

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

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

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

布局(performLayout)

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        final View host = mView;
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    }

    public void layout(int l, int t, int r, int b) {
  
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
        }
       
    }

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

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

绘画(performDraw)

 private void performDraw() {
        boolean canUseAsync = draw(fullRedrawNeeded);
    }

    private boolean draw(boolean fullRedrawNeeded){
     if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
      if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
          }
      }
         return useAsyncReport;

    }

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

        mView.draw(canvas);
        return true;
    }

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        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
        drawBackground(canvas);

        // Step 2, save the canvas' layers
        canvas.saveUnclippedLayer..

        // Step 3, draw the content
        onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        canvas.drawRect..

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

    }

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

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

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

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

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

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

总结

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

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

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

  • 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

感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—码上积木❤️ 每日一个知识点,建立完整体系架构。

本文分享自微信公众号 - 码上积木(Lzjimu),作者:积木zz

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-04-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • View绘制流程

    1. View 树的绘图流程 当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始...

    xiangzhihong
  • Latex绘制流程图

    我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite...

    EltonZheng
  • View的绘制流程之MeasureSpec

    目的 我在一个多月之前就说我准备开始梳理基础的事,好吧,我承认这一个月没我怎么梳理。或者梳理的不多,当我梳理到view的时候,发现需要分成绘制流程以及事件分发进...

    我就是马云飞
  • android View层的绘制流程

    还记得前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇文章吗?我们有分析到Activity中界面加...

    xiangzhihong
  • Android view绘制流程分析

    我们刚接触android开发的时候,应该都是从写布局开始的,在写布局的时候一般组长都要求我们少嵌套,这个是为什么呢?这个就要从我们今天要分析的invalidat...

    曾大稳
  • Android View绘制流程分析

    我们刚接触android开发的时候,应该都是从写布局开始的,在写布局的时候一般组长都要求我们少嵌套,这个是为什么呢?这个就要从我们今天要分析的invalidat...

    曾大稳
  • flowchart.js徒手绘制流程图

    要画20个流程(时序)图,于是昨天捣鼓了到半夜,安装了plantUML + vscode,虽然丑了些,但勉强能看,目前已用plantUML完成了10个。...

    周星星9527
  • Android界面绘制流程(一)

    aruba
  • Android界面绘制流程(二)

    aruba
  • iOS --- 简单的任务绘制复盘

    iOS图形绘制以及文本绘制一直是lz避免触及的地方,不为别的就是感觉这个东西不够对象化,比较零散。但因这次项目中遇到了这么个表达进度而又不是找不到现实UI库的情...

    大话swift
  • flutter窗口初始和绘制流程详析

    这里关注的是C++层面的绘制流程,平台怎样驱动和响应绘制与渲染的过程,并不是Dart部分的渲染。

    砸漏
  • OpenGL 系列---基础绘制流程

    OpenGL 是一种应用程序编程接口,它是一种可以对图形硬件设备特性进行访问的软件库。

    glumes
  • View的绘制-draw流程详解

    根据 measure 测量出的宽高,layout 布局的位置,渲染整个 View 树,将界面呈现出来。

    用户5546570
  • iOS CPU异步绘制的流程

    我们知道iOS界面渲染需要依靠强大图像计算能力的GPU,但是GPU并不是万能的,过分依赖GPU往往会导致GPU的性能出现瓶颈,要么导致离屏渲染,严重时还会出现卡...

    展菲
  • 开源计划之--Android绘图库--LogicCanvas

    Painter采用单例模式 优化原型模式,各Shape采用深拷贝来解决构造较长、繁琐的情况 比较new 对象和拷贝的效率问题,拷贝一点。具体见文:来谈谈Ja...

    张风捷特烈
  • View的绘制流程源码分析

    概述 View的绘制流程主要是指测量、布局以及绘制显示,在View中,measure是测量View的宽高,layout是控制View四个顶点的位置,而draw就...

    我就是马云飞
  • 绘制流程图的两种方法

    对于上班族可以使用流程图对某项工作操作过程绘制流程图进行归纳。对于科研工作者会将实验流程清晰的记录下来保证整个实验思维清晰,对于求职者可以将简历用流程图样式进行...

    用户5791017
  • View绘制流程深度剖析之-inlate

    LayoutInflater.inflate方法基本上每个开发者都用过,也有很多开发者了解过它的两个方法的区别,也有一些开发者去研究过源码,我这里再重复分析这个...

    陈宇明
  • Android中View绘制流程详细介绍

    Window即窗口,这个概念在AndroidFramework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗...

    砸漏

扫码关注云+社区

领取腾讯云代金券