requestLayout()的执行流程

本文详细解析了Android中View的绘制流程,从requestLayout方法出发,深入探讨了performMeasure、performLayout及performDraw方法的具体实现过程。

每次更新View都会想到requestLayout()方法,所以想看看它的流程是怎样的。
我一直觉得,先理清楚步骤再去看源码会好很多,所以先给出requestLayout()的一个调用流程:

  • View#requestLayout()

  • ViewGroup#requestLayout()

  • ViewRootImpl#requestLayout()

  • ViewRootImpl#scheduleTraversals()

  • ViewRootImpl#doTraversal()

  • ViewRootImpl#performTraversals()

最后ViewRootImpl#performTraversals()会依次调用performMeasure() , performLayout() , performDraw()。

requestLayout()是在View中定义的,View#requestLayout()

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()) {            if (!viewRoot.requestLayoutDuringLayout(this)) {                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }    /**记住这里设置的标志位,非常重要...**/
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;    if (mParent != null && !mParent.isLayoutRequested()) {        /**画重点,这里调用父类的requestLayout,一直往上循环...**/
        mParent.requestLayout();
    }    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

ViewParent是一个接口,定义了view的一些操作等方法。ViewGroup就是一个ViewParent,所以mParent.requestLayout会一直往上遍历,而终点是ViewRootImpl,ViewRootImpl#requestLayout()

// 入口方法,接下来执行 scheduleTraversals();public void requestLayout() {    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}// 会执行mTraversalRunnable,它是一个Runnablevoid scheduleTraversals() {    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ......
    }
}// 诺,TraversalRunnable 的定义。所以最后执行了doTraversal();final class TraversalRunnable implements Runnable {    @Override
    public void run() {
        doTraversal();
    }
}// 重点还不在这,重点是 performTraversals();!!!!void doTraversal() {    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        performTraversals();
        ......
    }
}

performTraversals()真的是一个很长很长的方法...,我平时写的一个类一般都不会有这么多。所以非常难去理解里面到底做了什么,所以我只是看了个大概的流程。

private void performTraversals() {    final View host = mView;    if (host == null || !mAdded)        return;
        ...... // 此处省略了很多行。。。
       
        if (!mStopped || mReportNextDraw) {            boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                    (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                    updatedConfiguration) {                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);                /**终于出现了:performMeasure**/
                // Ask host how big it wants to be
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ......
            }
        }
    } else {
        maybeHandleWindowMove(frame);
    }    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);    boolean triggerGlobalLayoutListener = didLayout
            || mAttachInfo.mRecomputeGlobalAttributes;    if (didLayout) {        /**这里是performLayout**/
        performLayout(lp, mWidth, mHeight);
        ......
    }
    ...... // 此处省略了很多行。。。
    if (!cancelDraw && !newSurface) {        if (mPendingTransitions != null && mPendingTransitions.size() > 0) {            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
        }        /**这里是performDraw()**/
        performDraw();
    } else {
        ......
    }
    mIsInTraversal = false;
}

本来想看看performTraversals()具体执行了些什么,后来我放弃了...,我只是找出了跟View绘制相关的三个方法:performMeasure() performLayout() performDraw()。
ViewRootImpl#performMeasure()

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {    if (mView == null) {        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

直接就调用了view的measure方法,从ViewRootImpl回到了View。View#measure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ......    /**还记得requestLayout方法里有把mPrivateFlags  = PFLAG_FORCE_LAYOUT**/
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);    /**依据forceLayout 和needsLayout决定是否要执行onMeasure**/
    if (forceLayout || needsLayout) {        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
        resolveRtlPropertiesIfNeeded();        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);        if (cacheIndex < 0 || sIgnoreMeasureCache) {            // measure ourselves, this should set the measured dimension flag back
            /**出现了onMeasure**/
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } 
        /**记住这里设置的标识位**/
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        ......
}

调用requestLayout时,设置了标识位mPrivateFlags  = PFLAG_FORCE_LAYOUT,最终这个view会根据mPrivateFlags 来判断是否要执行onMeasure方法。
那么最后还mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;,这个接下来onLayout会需要。
ViewRootImpl#performLayout()

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,        int desiredWindowHeight) {
    ......    final View host = mView;    if (host == null) {        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");    try {
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        ......
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}

host是view,那么performLayout就执行了view的layout(),View#layout()

public void layout(int l, int t, int r, int b) {
    ......    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);
       ......
    }
    ......
}

这里根据条件执行onLayout,上一步执行measure时,mPrivateFlags |= PFLAG_LAYOUT_REQUIRED。
ViewRootImpl#performDraw()

private void performDraw() {
    ......
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ......
}// 省略了很多行的代码,心好累。。。private void draw(boolean fullRedrawNeeded) {
    ......
    mAttachInfo.mDrawingTime =
            mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        ......
        } else {
            ......            /**你敢信onDraw的逻辑竟然隐藏在这里?**/
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {                return;
            }
        }
    }
    ......
}private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,        boolean scalingRequired, Rect dirty) {    final Canvas canvas;    try {
        .....        // 意外找到了Canvas的赋值地点
        canvas = mSurface.lockCanvas(dirty);
        .....
    } catch (Surface.OutOfResourcesException e) {
        handleOutOfResourcesException(e);        return false;
    } catch (IllegalArgumentException e) {
        mLayoutRequested = true;    // ask wm for a new surface next time.
        return false;
    }    try {
        ......        try {
            ......            /**终于找到你**/
            mView.draw(canvas);
            drawAccessibilityFocusedDrawableIfNeeded(canvas);
        } finally {
           ......
        }
    } finally {
       ......
    }    return true;
}

因此,逻辑是:performDraw()->draw()->drawSoftware()->view.draw()。
View#draw()

public 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 (绘制子view)
    *      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);
        drawAutofilledHighlight(canvas);        // 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);        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);        if (debugDraw()) {
            debugDrawFocus(canvas);
        }        // we're done...
        return;
    }

注释真好,这个方法的逻辑一下子就看懂了。

总结
  1. 当我们调用requestLayout时,会执行parent的requestLayout,最终执行到ViewRootImpl的requestLayout。

  2. ViewRootImpl经过一系列方法的调用执行performTraversals()方法。

  3. performTraversals()会依次执行performMeasure() performLayout() performDraw()方法。

看起来很简单,但真的会被源码转晕。而且实际调试过程中发现,View的draw()方法不一定是drawSoftware()执行的,可以看看ThreadedRenderer#draw()
mPrivateFlags 挺重要的,会根据它的值来决定一些方法是否要调用。

其实忽略了的代码中,隐藏着很重要的逻辑,主要是对draw()方法的执行判断。因为代码通篇看下来执行requestLayout后,onMeasure(),onLayout(),onDraw()方法会依次执行,onMeasure()和onLayout()执行是可以肯定的。但是onDraw()就不一定了。这部分我还没看懂,果然源码不简单。。。



作者:zcbiner
链接:https://www.jianshu.com/p/c25b497df8be
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

                        喜欢 就关注吧,欢迎投稿!

640?wx_fmt=jpeg



<think>我们正在处理一个关于Android ViewGroup中requestLayout方法无效的问题。根据用户提供的参考引用,我们可以分析原因并找出解决方案。 首先,根据引用[1]和引用[2]的描述,requestLayout方法会从当前View向上传递,直到ViewRootImpl。在这个过程中,每一层的父容器都会被打上PFLAG_FORCE_LAYOUT标记位,最终触发ViewRootImpl的requestLayout方法。如果这个传递过程被中断,或者ViewRootImpl没有正确处理,就可能导致requestLayout无效。 另外,引用[3]提到,在View的setFrame方法中,如果尺寸发生了变化,会调用invalidate(sizeChanged)来触发绘制。所以,即使requestLayout成功触发了测量和布局,如果没有绘制,用户也可能看不到变化。但requestLayout本身应该会触发整个流程(测量、布局、绘制)。 引用[4]则提到ViewGroup的onDraw方法可能没有被调用,这可能会导致即使布局发生了变化,也没有绘制出来。 因此,可能的原因包括: 1. requestLayout的传递被中断,没有到达ViewRootImpl。 2. 测量或布局过程中,View的尺寸或位置并没有实际改变,导致invalidate没有被调用。 3. ViewGroup本身没有触发绘制(例如,没有设置背景或setWillNotDraw(false)),导致onDraw没有被调用,从而看不到变化。 接下来,我们逐步分析原因并提供解决方案。 ### 原因分析 1. **传递中断**:如果当前View的父容器没有正确实现requestLayout方法,或者父容器本身已经被移除(例如,从父View中removeView了),那么requestLayout的传递可能会中断,导致ViewRootImpl没有收到请求。 2. **标记位被清除**:在布局过程中,如果父容器已经处理了布局请求(即已经清除了PFLAG_FORCE_LAYOUT标记),那么子View的requestLayout可能不会再次触发布局。 3. **尺寸未改变**:在setFrame方法中,只有尺寸或位置发生改变时,才会调用invalidate,从而触发绘制。如果requestLayout触发了measure和layout,但是计算结果和之前一样,那么就不会调用invalidate,因此不会重绘,用户可能看不到变化。 4. **ViewGroup默认不绘制自身**:ViewGroup默认情况下,如果没有设置背景,会设置一个标记(setWillNotDraw(true)),这样就不会调用onDraw方法。如果自定义ViewGroup需要在自身内容上绘制,但没有调用setWillNotDraw(false),那么即使调用了invalidate,也不会执行onDraw,导致看不到变化。 ### 解决方案 1. **确保View在视图树中**:检查调用requestLayout的View是否已经被添加到窗口上(即attachToWindow)。如果View已经被移除,那么requestLayout将无效。 2. **检查父容器的实现**:如果是自定义ViewGroup,确保没有覆盖requestLayout方法并且没有调用super.requestLayout(),否则会中断传递。 3. **多次调用requestLayout**:在某些情况下,可能需要调用多次requestLayout,因为Android系统可能会合并布局请求。可以尝试在调用requestLayout后,再调用invalidate()强制重绘。 4. **确保尺寸或位置改变**:检查在layout过程中,View的尺寸或位置是否发生了改变。如果没有改变,那么可能不会触发重绘,此时可以手动调用invalidate。 5. **设置setWillNotDraw(false)**:对于自定义ViewGroup,如果需要在onDraw中绘制内容,那么必须在构造方法中调用setWillNotDraw(false),否则onDraw不会被调用。 6. **检查是否在UI线程**:确保在主线程中调用requestLayout,因为Android的UI操作必须在主线程进行。 7. **使用post方法延迟调用**:有时在View还未完全附加到窗口时调用requestLayout可能无效,可以使用post方法延迟调用: ```java view.post(new Runnable() { @Override public void run() { view.requestLayout(); } }); ``` ### 总结 当`requestLayout`无效时,通常需要检查以下几个方面: - 视图是否已附加到窗口 - 自定义ViewGroup是否正确实现了requestLayout - 是否需要在尺寸未改变时强制重绘(手动调用invalidate) - 自定义ViewGroup是否需要关闭`setWillNotDraw` 如果以上步骤都检查无误,那么可能需要进一步分析具体的布局流程和绘制流程是否被正确触发。 §§相关问题§§ 1. 在Android中,如何判断一个View是否已经附加到窗口? 2. ViewGroup的requestLayout方法在什么情况下会被中断? 3. 如何强制View进行重绘(即使尺寸没有改变)? 4. 自定义ViewGroup时,为什么onDraw方法没有被调用?如何解决? </think>### Android ViewGroup requestLayout方法无效的原因及解决方案 #### 一、核心原因分析 1. **布局请求未传递到ViewRootImpl** - requestLayout采用责任链模式向上传递(子View → 父容器 → DecorView → ViewRootImpl)[^2] - 中断场景: * 父容器`mParent==null`(View未Attach到窗口) * 父容器已标记`isLayoutRequested()`(请求被合并) * 自定义ViewGroup未正确调用`super.requestLayout()` 2. **标记位冲突** - 当同时存在`PFLAG_FORCE_LAYOUT`(强制布局)和`PFLAG_INVALIDATED`(重绘)标记时,系统可能优先处理重绘[^3] - `setFrame()`中尺寸未变化时不会触发`invalidate(true)`[^3] 3. **ViewGroup绘制机制限制** - ViewGroup默认开启`setWillNotDraw(true)`,跳过自身onDraw()[^4] - 尺寸变更后若未触发重绘,视觉无变化(即使布局已更新) #### 二、解决方案 1. **基础检查** ```java // 确保View已附加到窗口 if (view.isAttachedToWindow()) { view.requestLayout(); } else { view.getViewTreeObserver().addOnGlobalLayoutListener(() -> { view.requestLayout(); }); } ``` 2. **强制传递布局请求** ```java // 在自定义ViewGroup中重写requestLayout @Override public void requestLayout() { super.requestLayout(); // 必须调用父类方法 if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); // 手动向上传递 } } ``` 3. **标记位同步处理** ```java view.forceLayout(); // 强制设置PFLAG_FORCE_LAYOUT view.invalidate(); // 添加PFLAG_INVALIDATED view.requestLayout(); ``` 4. **自定义ViewGroup关键配置** ```java public CustomViewGroup(Context context) { super(context); setWillNotDraw(false); // 允许执行onDraw } @Override protected boolean setFrame(int l, int t, int r, int b) { boolean changed = super.setFrame(l, t, r, b); if (changed) invalidate(); // 尺寸变化强制重绘 return changed; } ``` #### 三、调试建议 1. 使用`getParent()`递归检查请求传递链 2. 重写`onLayout()`添加日志验证是否被调用 3. 检查`View.isLayoutRequested()`返回值 4. 监控`ViewTreeObserver.OnGlobalLayoutListener` > 典型案例:当View尺寸未实际改变时,`setFrame()`返回false导致不触发重绘[^3],此时需手动调用`invalidate()`同步视觉更新。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值