首页 > 编程语言 >Android Measure,Layout,Draw 源码阅读

Android Measure,Layout,Draw 源码阅读

时间:2022-08-28 20:47:50浏览次数:75  
标签:Draw layout int requestLayout 源码 measure Layout performTraversals view

Android Measure,Layout,Draw 源码阅读

Android View的测量、布局、绘制过程详解(上)_>进阶的程序员>的博客-CSDN博客

Android View的测量、布局、绘制过程详解(下)_>进阶的程序员>的博客-CSDN博客

根据这两篇文章,我对 measure/layout/draw 的原理有了初步了解, 但这系列函数调用的时机还不是很清楚,以及他们与view生命周期的关系

scheduleTraversals

通过阅读源码, 选择从ViewRootImpl的scheduleTraversals函数入手

// ViewRootImpl.java
void scheduleTraversals() {
    // 防止重入
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 关键代码,注册 vsync mTraversalRunnable 回调 
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

// 一条初始化的调用路径
PhoneWindow::setContentView()
View::requestApplyInsets()
ViewParent::requestFitSystemWindows()

// 其他调用场景 ViewRootImpl.java
requestLayout()
invalidate()
{request,clear}ChildFocus()
handleAppVisibility()
handleGetNewSurface()
DisplayListener::onDisplayChanged()

注册完成后会在 vsync 发生后触发 doTraversal

void doTraversal() {
    // 下一帧可以继续 doTraversal
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        // 移除 sync barrier
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // method tracing
        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

performTraversals() 就是要找的函数, measurelayoutdraw 三大函数都在这里完成调用

Measure

ViewRootImple 有一个 mFirst 标记了对 performTraversals 的第一次调用,我们以此为线索, 来分析他的内部逻辑

// ViewRootImpl.java
private void performTraversals() {
    
    WindowManager.LayoutParams lp = mWindowAttributes;
    
    // :1732  1.算宽高
    Rect frame = mWinFrame;
    if (mFirst) {
        // 第一次必须要 layout + redraw
        mFullRedrawNeeded = true;
        mLayoutRequested = true;
        
        // 算可用的 window size
        final Configuration config = mContext.getResources().getConfiguration();
        if (shouldUseDisplaySize(lp)) {
            // NOTE -- system code, won't try to do compat mode.
            Point size = new Point();
            mDisplay.getRealSize(size);
            desiredWindowWidth = size.x;
            desiredWindowHeight = size.y;
        } else {
            desiredWindowWidth = mWinFrame.width();
            desiredWindowHeight = mWinFrame.height();
        }
        
        // AttachedToWindow, 这里也会做很多事情
        // ... 
        // Set the layout direction if it has not been set before (inherit is the default)
        if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
            host.setLayoutDirection(config.getLayoutDirection());
        }
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
    }

    // :1803 mesure
    boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
    if (layoutRequested) {
    
        final Resources res = mView.getContext().getResources();
        // ...
        // Ask host how big it wants to be
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
    }
    
    // :1904 mesure 2    
    // 这个 flag 在 requestFitSystemWindows 中设置
    // TODO 为什么要 measure 两次 
    if (mApplyInsetsRequested) {
        mApplyInsetsRequested = false;
        mLastOverscanRequested = mAttachInfo.mOverscanRequested;
        dispatchApplyInsets(host);
        if (mLayoutRequested) {
            // Short-circuit catching a new layout request here, so
            // we don't need to go through two layout passes when things
            // change due to fitting system windows, which can happen a lot.
            windowSizeMayChange |= measureHierarchy(host, lp,
                    mView.getContext().getResources(),
                    desiredWindowWidth, desiredWindowHeight);
        }
    }

    if (layoutRequested) {
        // Clear this now, so that if anything requests a layout in the
        // rest of this function we will catch it and re-run a full
        // layout pass.
        mLayoutRequested = false;
    }    
}

先从 performTraversals 跳出,看一下 measure 的逻辑 (去掉debug-log)

private 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;
    
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        // 这里的逻辑是针对 大屏中的 dialog, WRAP_CONTENT 不需要 match 整个屏幕
        // 如果 measure 成功就会设置 goodMeasure 为 true
        // dialog 与底层的 view 不是一个 window, 因此会有独立的 ViewRoot
        // ...
    }
    
    // 我们的逻辑肯定会进到这里
    // getRootMeasureSpec 就是将  layout param + size 转化拼装成 MeasureSpec
    // WRAP_CONTENT => AT_MOST, other => EXACTLY
    if (!goodMeasure) {
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        // 这个函数就是带trace的 host.measure 方法调用
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        // 判断 measure 完的 size 是否发生改变
        // 发生改变后的逻辑 会推迟到 windowSizeMayChange 中
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }

    return windowSizeMayChange;
}

接下来就是 view.measure , 这是一个 final 方法,它会帮你规避不必要的重调与缓存调用结果, 真正的 measure 逻辑是在 onMeasure 中完成的,默认的实现就是直接设置自身宽高, 自定义view需要重写该方法来更好的测量自身, 可以看一下它的注释

Measure the view and its content to determine the measured width and the measured height. This method is invoked by measure(int, int) and should be overridden by subclasses to provide accurate and efficient measurement of their contents.

CONTRACT: When overriding this method, you must call setMeasuredDimension(int, int) to store the measured width and height of this view. Failure to do so will trigger an IllegalStateException, thrown by measure(int, int). Calling the superclass' onMeasure(int, int) is a valid use.

view实现该方法的形式五花八门,有两个基本的contract:

  • 调用子view的measure 而非 onMeasure,

  • 测量完成后通过 setMeasuredDimension 更新 measured{width,height}

Layout

现在回到 performTraversals, 中间有一段window相关逻辑,我们也不看了,直奔layout

// ViewRootImpl.java
private void performTraversals() {
    // :2315
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    boolean triggerGlobalLayoutListener = didLayout
            || mAttachInfo.mRecomputeGlobalAttributes;
    if (didLayout) {
        // mWidth, mHeight 直接是 mWinFrame 的 size
        // 可见 :2195
        performLayout(lp, mWidth, mHeight);
        // By this point all views have been sized and positioned
        // We can compute the transparent area
        // ...
    }
}

performLayout 同样是 host.layout 的 traced wrap caller,

但为了防止 layout 过程中子view再调用request layout,损坏内部状态, android 为我们做了处理,

逻辑就是在 layout 过程中有 requestLayout call 的 view 都加入一个列表中,layout 返回后如果子view还的flag还包含 requestLayout, 就再做一边 requestlayout/measure/layout , 如果做完这些还存在 requestLayout 的 view, 则post到 runqueue 等下一次时机再让 view 做 requestLayout. 具体可见 requestLayoutDuringLayout 的注释.

可能有点绕, 先插队看下 requestLayout 到底干了啥

// View.java
@CallSuper
public void requestLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();

    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null && viewRoot.isInLayout()) {
            // 这里就是 ViewRootImpl layout 中就加 list
            if (!viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;
    // 查看 mPrivateFlags 是否被设置了 PFLAG_FORCE_LAYOUT
    if (mParent != null && !mParent.isLayoutRequested()) {
        // 如果父view的标志位没有设置,就向上调用
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

// ViewRootImpl.java
@Override
public void requestLayout() {
    // 最终调用到这里, 再走一遍 scheduleTraversals
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

可见 requestLayout 就是标记自身 dirty,为layout提供信息,然后向上调用到ViewRootImpl::requestLayout 在下一帧触发view tree重新layout。

然后来看一下layout, 它会设置view 的上下左右属性,再调用onLayout, 给自定义view做自己的逻辑(通常是layout 子 view)

// View.java
public void layout(int l, int t, int r, int b) {
    // PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT 判断是否要调用 onMeasure
    // 记录老的 上下左右

    // setFrame: 更新 上下左右
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);

        // ...
        // 触发 onLayoutChange 回调
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
    // 焦点相关
    // PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT
}

Draw

最后一步是绘制 view 调用链如下

performTraversals 
         ||
         \/
     performDraw 
         ||
         \/
        draw    ===========================
         ||                              \  \
         \/                                \\
mAttachInfo.mThreadedRenderer.draw        drawSoftware
         ||                                   ||
         \/                                   \/
ThreadedRenderer.updateRootDisplayList    View.draw(canvas)
         ||                                   ||
         \/                                   ||
updateRootDisplayList                         ||
         ||                                   ||
         \/                                   ||
View.updateDisplayListIfDirty                 ||
         ||                                   ||
         \/                                   \/
                      View.onDraw(canvas

右边的路径可能比较熟悉, 直接创建 canvas 并交给 view 去做绘制,它的名字叫软绘,

那么相应的左面的绘制就是GPU绘制了, 简单概述它的流程就是通过 view的RenderNode去创建DisplayListCanvas, 然后view生成displaylist,记录到canvas上, 最后交给native gl渲染。
由于DisplayListCanvas 继承了 Canvas, 因此可以适配软绘的逻辑.

还有一点要注意的是 ViewRootImpl.draw 只会重绘 dirty 的部分, 因此需要调用 View.invalidate 向上报告 dirty 区域, 从而在下次 performTraversals 绘制到需要重绘的部分

总结

ViewRootImpl(VRI) 处在整个 viewtree的根部,

  • performTraversals 负责触发 measure, layout, draw

  • measure : 在给定大小中测量 view 应占宽高 width, height

  • layout : 设置 view 相对于 parent 的位置与空间 Left,right,top,bottom

  • draw : 给 view 一个 canvas, 让他绘制自己

  • scheduleTraversals 向 Choreographer 注册 vsync 信号回调,在信号发生后调用 performTraversals ,保证一帧只会触发一次。 (第一次触发时机,activity调用: setContentView)

view 通过

  • requestLayout(触发scheduleTraversals, 类似于reflow)

  • invalidate(更新dirty rect, 类似于repaint)

向上报告,直到VRI

常见问题: addView 或修改view属性后拿不到更新后的宽高等信息

由于 performTraversals 需要 vsync 触发, 在requestLayout之后还要等一个异步时机,因此可以在它之后 post 一个回调, 消息队列保证了前后关系, 因此在回调中可以正确拿到 view measure/layout 后的信息

标签:Draw,layout,int,requestLayout,源码,measure,Layout,performTraversals,view
From: https://www.cnblogs.com/xxrlz/p/16633575.html

相关文章

  • 重新构建rocketmq_exporter源码,构建镜像
    1.githubhttps://github.com/apache/rocketmq-exporter 2.dockerfileFROMmaven:3.8.6-openjdk-8-slimCOPYrocketmq-exporter-master/apps/rocketmq-exporter-mas......
  • 重新编译influxdb_exporter源码,构建镜像
    1.githubhttps://github.com/prometheus/influxdb_exporter 2.dockerfileFROMgolang:1.17ENVGO111MODULE=on\GOPROXY="https://goproxy.cn,direct"COPYin......
  • 重新编译kafka_exporter源码,构建镜像
    1.githubhttps://github.com/danielqsj/kafka_exporter 2.dockerfileFROMgolang:1.17ENVGO111MODULE=on\GOPROXY="https://goproxy.cn,direct"COPYkafka_......
  • 重新编译activemq_exporter源码,构建镜像
    1.githubhttps://github.com/prometheus/jmx_exporteractivemq使用的是jmx_exporter来监控,0.17.0版本才有jmx_prometheus_httpserver 2.dockerfileFROMopenjdk:alp......
  • 重新编译jmx_exporter源码,构建镜像
    1.githubhttps://github.com/prometheus/jmx_exporter 2.dockerfileFROMopenjdk:alpineCOPYjmx_exporter-parent-0.16.1/apps/jmx_exporter-parent-0.16.1WORKD......
  • 为什么Go源码中有些函数没有函数体?
    在Go源码中,有时候我们点开查看,会发现这样的东西:这些是没有函数体的,这是为什么呢?这些是runtime的,也就是实现不是用Go写的,这一类方法,有些用汇编写的,有一些用C写的,可......
  • Spring源码-自定义标签
    一、新建实体类publicclassUserimplementsSerializable{privateStringid;privateStringname;privateIntegerage;publicStringgetId(){ return......
  • spring源码具体细节 super setConfigLocations
      1首先先调用super父类构造方法 classPathXmlApplicaitonContext 初始化成员属性  依然掉父类构造方法 调用父类 资源处理器 当前系统需要运行所......
  • Spring源码01:环境搭建
    写在开始:这个系列会陆续更新我学习Spring源码的一些笔记与自己的理解,由于本人水平有限,难免对有些知识理解不到位,亦或者手误导致文中有不正确的地方,欢迎各位指正与探讨。......
  • 专业企业微信第三方平台源码免费领取企微魔盒创业版
    系统简介:企微魔盒是一款专业的企业微信第三方源码系统,可私有化部署到自己服务器,数据自己掌握。付费版自带多种丰富的模块,如会话存档,裂变宝,红包获客,互动雷达,互动红包,客户阶......