Android View 深度分析requestLayout、invalidate與postInvalidate

前言

前幾篇文章中,筆者對View的三大工作流程進行了詳細分析,而這篇文章則詳細講述與三大工作流程密切相關的兩個方法,分別是requestLayout和invalidate,如果對Viwe的三個工作流程不熟悉的讀者,可以先看看前幾篇文章,以便能更容易理解這篇文章的內容。

requestLayout

當我們動態移動一個View的位置,或者View的大小、形狀發生了變化的時候,我們可以在view中調用這個方法,即:

view.requestLayout();

那麼該方法的作用是什麼呢?
從方法名字可以知道,“請求佈局”,那就是說,如果調用了這個方法,那麼對於一個子View來說,應該會重新進行佈局流程。但是,真實情況略有不同,如果子View調用了這個方法,其實會從View樹重新進行一次測量、佈局、繪製這三個流程,最終就會顯示子View的最終情況。那麼,這個方法是怎麼實現的呢?我們從源碼角度進行解析。
首先,我們看View#requestLayout方法:

/**
 * Call this when something has changed which has invalidated the
 * layout of this view. This will schedule a layout pass of the view
 * tree. This should not be called while the view hierarchy is currently in a layout
 * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
 * end of the current layout pass (and then layout will run again) or after the current
 * frame is drawn and the next layout occurs.
 *
 * <p>Subclasses which override this method should call the superclass method to
 * handle possible request-during-layout errors correctly.</p>
 */
//從源碼註釋可以看出,如果當前View在請求佈局的時候,View樹正在進行佈局流程的話,
//該請求會延遲到佈局流程完成後或者繪製流程完成且下一次佈局發現的時候再執行。
@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()) {
            if (!viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    //爲當前view設置標記位 PFLAG_FORCE_LAYOUT
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) {
        //向父容器請求佈局
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

在requestLayout方法中,首先先判斷當前View樹是否正在佈局流程,接着爲當前子View設置標記位,該標記位的作用就是標記了當前的View是需要進行重新佈局的,接着調用mParent.requestLayout方法,這個十分重要,因爲這裏是向父容器請求佈局,即調用父容器的requestLayout方法,爲父容器添加PFLAG_FORCE_LAYOUT標記位,而父容器又會調用它的父容器的requestLayout方法,即requestLayout事件層層向上傳遞,直到DecorView,即根View,而根View又會傳遞給ViewRootImpl,也即是說子View的requestLayout事件,最終會被ViewRootImpl接收並得到處理。縱觀這個向上傳遞的流程,其實是採用了責任鏈模式,即不斷向上傳遞該事件,直到找到能處理該事件的上級,在這裏,只有ViewRootImpl能夠處理requestLayout事件。

在ViewRootImpl中,重寫了requestLayout方法,我們看看這個方法,ViewRootImpl#requestLayout:

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

在這裏,調用了scheduleTraversals方法,這個方法是一個異步方法,最終會調用到ViewRootImpl#performTraversals方法,這也是View工作流程的核心方法,在這個方法內部,分別調用measure、layout、draw方法來進行View的三大工作流程,對於三大工作流程,前幾篇文章已經詳細講述了,這裏再做一點補充說明。
先看View#measure方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
     ...

    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {
        ...
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } 
        ...
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
}

首先是判斷一下標記位,如果當前View的標記位爲PFLAG_FORCE_LAYOUT,那麼就會進行測量流程,調用onMeasure,對該View進行測量,接着最後爲標記位設置爲PFLAG_LAYOUT_REQUIRED,這個標記位的作用就是在View的layout流程中,如果當前View設置了該標記位,則會進行佈局流程。具體可以看如下View#layout源碼:

public void layout(int l, int t, int r, int b) {
    ...
    //判斷標記位是否爲PFLAG_LAYOUT_REQUIRED,如果有,則對該View進行佈局
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        //onLayout方法完成後,清除PFLAG_LAYOUT_REQUIRED標記位
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    //最後清除PFLAG_FORCE_LAYOUT標記位
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

那麼到目前爲止,requestLayout的流程便完成了。

小結:子View調用requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會調用三大流程,從measure開始,對於每一個含有標記位的view及其子View都會進行測量、佈局、繪製。

invalidate

該方法的調用會引起View樹的重繪,常用於內部調用(比如 setVisiblity())或者需要刷新界面的時候,需要在主線程(即UI線程)中調用該方法。那麼我們來分析一下它的實現。
首先,一個子View調用該方法,那麼我們直接看View#invalidate方法:

public void invalidate() {
    invalidate(true);
}
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 (mGhostView != null) {
        mGhostView.invalidate(true);
        return;
    }

    //這裏判斷該子View是否可見或者是否處於動畫中
    if (skipInvalidate()) {
        return;
    }

    //根據View的標記位來判斷該子View是否需要重繪,假如View沒有任何變化,那麼就不需要重繪
    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)) {
        if (fullInvalidate) {
            mLastIsOpaque = isOpaque();
            mPrivateFlags &= ~PFLAG_DRAWN;
        }

        //設置PFLAG_DIRTY標記位
        mPrivateFlags |= PFLAG_DIRTY;

        if (invalidateCache) {
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        // 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);
            //調用父容器的方法,向上傳遞事件
            p.invalidateChild(this, damage);
        }
        ...
    }
}

可以看出,invalidate有多個重載方法,但最終都會調用invalidateInternal方法,在這個方法內部,進行了一系列的判斷,判斷View是否需要重繪,接着爲該View設置標記位,然後把需要重繪的區域傳遞給父容器,即調用父容器的invalidateChild方法。
接着我們看ViewGroup#invalidateChild

/**
 * Don't call or override this method. It is used for the implementation of
 * the view hierarchy.
 */
public final void invalidateChild(View child, final Rect dirty) {

    //設置 parent 等於自身
    ViewParent parent = this;

    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // If the child is drawing an animation, we want to copy this flag onto
        // ourselves and the parent to make sure the invalidate request goes
        // through
        final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION)
                == PFLAG_DRAW_ANIMATION;

        // Check whether the child that requests the invalidate is fully opaque
        // Views being animated or transformed are not considered opaque because we may
        // be invalidating their old position and need the parent to paint behind them.
        Matrix childMatrix = child.getMatrix();
        final boolean isOpaque = child.isOpaque() && !drawAnimation &&
                child.getAnimation() == null && childMatrix.isIdentity();
        // Mark the child as dirty, using the appropriate flag
        // Make sure we do not set both flags at the same time
        int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;

        if (child.mLayerType != LAYER_TYPE_NONE) {
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        //儲存子View的mLeft和mTop值
        final int[] location = attachInfo.mInvalidateChildLocation;
        location[CHILD_LEFT_INDEX] = child.mLeft;
        location[CHILD_TOP_INDEX] = child.mTop;

        ...

        do {
            View view = null;
            if (parent instanceof View) {
                view = (View) parent;
            }

            if (drawAnimation) {
                if (view != null) {
                    view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                } else if (parent instanceof ViewRootImpl) {
                    ((ViewRootImpl) parent).mIsAnimating = true;
                }
            }

            // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
            // flag coming from the child that initiated the invalidate
            if (view != null) {
                if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                        view.getSolidColor() == 0) {
                    opaqueFlag = PFLAG_DIRTY;
                }
                if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                    //對當前View的標記位進行設置
                    view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                }
            }

            //調用ViewGrup的invalidateChildInParent,如果已經達到最頂層view,則調用ViewRootImpl
            //的invalidateChildInParent。
            parent = parent.invalidateChildInParent(location, dirty);

            if (view != null) {
                // Account for transform on current parent
                Matrix m = view.getMatrix();
                if (!m.isIdentity()) {
                    RectF boundingRect = attachInfo.mTmpTransformRect;
                    boundingRect.set(dirty);
                    m.mapRect(boundingRect);
                    dirty.set((int) (boundingRect.left - 0.5f),
                            (int) (boundingRect.top - 0.5f),
                            (int) (boundingRect.right + 0.5f),
                            (int) (boundingRect.bottom + 0.5f));
                }
            }
        } while (parent != null);
    }
}

可以看到,在該方法內部,先設置當前視圖的標記位,接着有一個do…while…循環,該循環的作用主要是不斷向上回溯父容器,求得父容器和子View需要重繪的區域的並集(dirty)。當父容器不是ViewRootImpl的時候,調用的是ViewGroup的invalidateChildInParent方法,我們來看看這個方法,ViewGroup#invalidateChildInParent:

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
            (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
        if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
                    FLAG_OPTIMIZE_INVALIDATE) {

            //將dirty中的座標轉化爲父容器中的座標,考慮mScrollX和mScrollY的影響
            dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                    location[CHILD_TOP_INDEX] - mScrollY);

            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                //求並集,結果是把子視圖的dirty區域轉化爲父容器的dirty區域
                dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
            }

            final int left = mLeft;
            final int top = mTop;

            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                    dirty.setEmpty();
                }
            }
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

            //記錄當前視圖的mLeft和mTop值,在下一次循環中會把當前值再向父容器的座標轉化
            location[CHILD_LEFT_INDEX] = left;
            location[CHILD_TOP_INDEX] = top;

            if (mLayerType != LAYER_TYPE_NONE) {
                mPrivateFlags |= PFLAG_INVALIDATED;
            }
            //返回當前視圖的父容器
            return mParent;

        }
        ...
    }
    return null;
}

可以看出,這個方法做的工作主要有:調用offset方法,把當前dirty區域的座標轉化爲父容器中的座標,接着調用union方法,把子dirty區域與父容器的區域求並集,換句話說,dirty區域變成父容器區域。最後返回當前視圖的父容器,以便進行下一次循環。

回到上面所說的do…while…循環,由於不斷向上調用父容器的方法,到最後會調用到ViewRootImpl的invalidateChildInParent方法,我們來看看它的源碼,ViewRootImpl#invalidateChildInParent:

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    checkThread();
    if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);

    if (dirty == null) {
        invalidate();
        return null;
    } else if (dirty.isEmpty() && !mIsAnimating) {
        return null;
    }

    if (mCurScrollY != 0 || mTranslator != null) {
        mTempRect.set(dirty);
        dirty = mTempRect;
        if (mCurScrollY != 0) {
            dirty.offset(0, -mCurScrollY);
        }
        if (mTranslator != null) {
            mTranslator.translateRectInAppWindowToScreen(dirty);
        }
        if (mAttachInfo.mScalingRequired) {
            dirty.inset(-1, -1);
        }
    }

    final Rect localDirty = mDirty;
    if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
        mAttachInfo.mSetIgnoreDirtyState = true;
        mAttachInfo.mIgnoreDirtyState = true;
    }

    // Add the new dirty rect to the current one
    localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
    // Intersect with the bounds of the window to skip
    // updates that lie outside of the visible region
    final float appScale = mAttachInfo.mApplicationScale;
    final boolean intersected = localDirty.intersect(0, 0,
            (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    if (!intersected) {
        localDirty.setEmpty();
    }
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();
    }
    return null;
}

可以看出,該方法所做的工作與上面的差不多,都進行了offset和union對座標的調整,然後把dirty區域的信息保存在mDirty中,最後調用了scheduleTraversals方法,觸發View的工作流程,由於沒有添加measure和layout的標記位,因此measure、layout流程不會執行,而是直接從draw流程開始。

好了,現在總結一下invalidate方法,當子View調用了invalidate方法後,會爲該View添加一個標記位,同時不斷向父容器請求刷新,父容器通過計算得出自身需要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程(只繪製需要重繪的視圖)。

postInvalidate

這個方法與invalidate方法的作用是一樣的,都是使View樹重繪,但兩者的使用條件不同,postInvalidate是在非UI線程中調用,invalidate則是在UI線程中調用。
接下來我們分析postInvalidate方法的原理。
首先看View#postInvalidate

public void postInvalidate() {
    postInvalidateDelayed(0);
}

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;
    if (attachInfo != null) {
        attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
    }
}

由以上代碼可以看出,只有attachInfo不爲null的時候纔會繼續執行,即只有確保視圖被添加到窗口的時候纔會通知view樹重繪,因爲這是一個異步方法,如果在視圖還未被添加到窗口就通知重繪的話會出現錯誤,所以這樣要做一下判斷。接着調用了ViewRootImpl#dispatchInvalidateDelayed方法:

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

這裏用了Handler,發送了一個異步消息到主線程,顯然這裏發送的是MSG_INVALIDATE,即通知主線程刷新視圖,具體的實現邏輯我們可以看看該mHandler的實現:

final ViewRootHandler mHandler = new ViewRootHandler();

final class ViewRootHandler extends Handler {
        @Override
        public String getMessageName(Message message) {
            ....
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_INVALIDATE:
                ((View) msg.obj).invalidate();
                break;
            ...
        }
    }
}

可以看出,參數message傳遞過來的正是View視圖的實例,然後直接調用了invalidate方法,然後繼續invalidate流程。

到目前爲止,對於常用的刷新視圖的方法已經分析完畢。最後以一幅流程圖來說明requestLayout、invalidate的區別:

requestlayout and invalidate.jpg
一般來說,如果View確定自身不再適合當前區域,比如說它的LayoutParams發生了改變,需要父佈局對其進行重新測量、佈局、繪製這三個流程,往往使用requestLayout。而invalidate則是刷新當前View,使當前View進行重繪,不會進行測量、佈局流程,因此如果View只需要重繪而不需要測量,佈局的時候,使用invalidate方法往往比requestLayout方法更高效。最後,感謝你們的閱讀,希望這篇文章給你們帶來幫助。

更多閱讀
Android View 測量流程(Measure)完全解析
Android View 佈局流程(Layout)完全解析
Android View 繪製流程(Draw) 完全解析

發佈了61 篇原創文章 · 獲贊 283 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章