Android自定義View之invalidate方法和postInvalidate方法

xxq2dream發表於2018-08-09

Android自定義View系列

我們在自定義View時免不了要使用invalidate方法,這個方法的作用大家也比較清楚,就是讓我們的View進行重新整理重新繪製的。但是postInvalidate方法可能就不是那麼熟悉了,因為平時開發時invalidate方法相對而言會用得比較多。不過需要大家注意的是,面試官在問到View相關的問題時,就很有可能會問到postInvalidate方法,所以我們還是有必要來學習一下。

那invalidate方法和postInvalidate方法到底有什麼區別呢?

invalidate方法和postInvalidate方法的區別

其實答案也很簡單,就一句話:

invalidate方法和postInvalidate方法都是用於進行View的重新整理,invalidate方法應用在UI執行緒中,而postInvalidate方法應用在非UI執行緒中,用於將執行緒切換到UI執行緒,postInvalidate方法最後呼叫的也是invalidate方法。

當然,空口無憑,我們還是來看看原始碼

invalidate方法和postInvalidate方法原始碼分析

我們先來看看View中的postInvalidate方法

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    
    ...
    
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    
    public void postInvalidate(int left, int top, int right, int bottom) {
        postInvalidateDelayed(0, left, top, right, bottom);
    }
    
    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
    ...      
}

複製程式碼

從原始碼中我們可以看到,postInvalidate方法最後呼叫了ViewRootImpl的dispatchInvalidateDelayed方法

//ViewRootImpl.class

final ViewRootHandler mHandler = new ViewRootHandler();

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

ViewRootImpl中的dispatchInvalidateDelayed方法就是像ViewRootHandler傳送了一個MSG_INVALIDATE訊息,ViewRootHandler是ViewRootImpl中的一個內部Handler類

final class ViewRootHandler extends Handler {
    @Override
    public String getMessageName(Message message) {
        switch (message.what) {
            case MSG_INVALIDATE:
                return "MSG_INVALIDATE";
            ...
        }
        return super.getMessageName(message);
    }

    ...

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

我們可以看到ViewRootHandler中對於MSG_INVALIDATE訊息的處理就是呼叫的View的invalidate方法。

總結

綜上原始碼我們可以得出結論:

(1)postInvalidate方法呼叫了ViewRootImpl中的dispatchInvalidateDelayed方法向ViewRootImpl中的ViewRootHandler傳送一個訊息,最後呼叫的還是View的invalidate方法。

(2)因為ViewRootImpl是在UI執行緒的,所以postInvalidate方法的作用就是將非UI執行緒的重新整理操作切換到UI執行緒,以便在UI執行緒中呼叫invalidate方法重新整理View。所以如果我們自定義的View本身就是在UI執行緒,沒有用到多執行緒的話,直接用invalidate方法來重新整理View就可以了。而我們平時自定義View基本上都沒有開起其他執行緒,所以這就是我們很少接觸postInvalidate方法的原因

(3)所以一句話總結postInvalidate方法的作用就是:實現了訊息機制,可以使我們在非UI執行緒也能呼叫重新整理View的方法。


invalidate方法重新整理View的呼叫過程分析

//View.class
@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {

    ...
    
    public void invalidate() {
        invalidate(true);
    }
    
    //invalidateCache為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 ((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;
            }

            mPrivateFlags |= PFLAG_DIRTY;

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

            //重點就在這裡!!!
            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);
            }

            ...
        }
    }
}

複製程式碼

View的invalidate方法最後呼叫的是invalidateInternal方法,invalidateInternal方法中的重點邏輯在上面已經標記出來了。

  • 其中的damage變數表示的是需要進行重繪的區域,後面在一系列的呼叫過程中會不斷根據父佈局來調整這個繪製區域。
  • invalidateInternal方法中通過呼叫View的父佈局invalidateChild方法來請求重繪。那View的父佈局是誰呢?這裡有2種情況:要麼是ViewGroup,要麼就是ViewRootImpl類了。

我們先來看看ViewGroup中的invalidateChild方法

@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    @Override
    public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;

        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            ...
            //這裡省略了一些重新計算繪製區域的邏輯

            //這是一個從當前的佈局View向上不斷遍歷當前佈局View的父佈局,最後遍歷到ViewRootImpl的迴圈
            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;
                    }
                }

                ...
                
                //這裡呼叫的是父佈局的invalidateChildInParent方法
                parent = parent.invalidateChildInParent(location, dirty);
                ...
            } while (parent != null);
        }
    }
    
    @Override
    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) {
                ...
                //這裡也是一些計算繪製區域的內容

                return mParent;

            } else {
                mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;

                ...
                //這裡也是一些計算繪製區域的內容

                return mParent;
            }
        }

        return null;
    }
}
複製程式碼

在ViewGroup的invalidateChild方法中有一個迴圈,迴圈裡面會一直呼叫父佈局的invalidateChildInParent方法,而View和ViewGroup的最終父佈局都是ViewRootImpl

所以View中的invalidateInternal方法和ViewGroup中的invalidateChild方法最後殊途同歸,都會呼叫到ViewRootImpl中的方法

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks {
    
    //如果View沒有父佈局,那invalidateInternal方法就會呼叫這個方法
    @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }

    //ViewGroup的invalidateChild方法最後會呼叫到這裡
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();

&emsp;&emsp;&emsp;&emsp;//如果dirty為null就表示要重繪當前ViewRootImpl指示的整個區域
        if (dirty == null) {
            invalidate();
            return null;
        //如果dirty為empty則表示經過計算需要重繪的區域不需要繪製
        } 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);
            }
        }

        invalidateRectOnScreen(dirty);

        return null;
    }   
    
    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        ...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            //呼叫scheduleTraversals方法進行繪製
            scheduleTraversals();
        }
    }
    
    //繪製整個ViewRootImpl區域
    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            //呼叫scheduleTraversals方法進行繪製
            scheduleTraversals();
        }
    }
}
複製程式碼

可以看到在ViewRootImpl中最後都會呼叫scheduleTraversals方法進行繪製。按照我們對於View的繪製過程的理解,View的繪製流程是從ViewRootImpl的performTraversals方法開始的,下面我們來看看ViewRootImpl中的scheduleTraversals方法。

//ViewRootImpl.class
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //關鍵在這裡!!!
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
複製程式碼

居然沒有我們要找的performTraversals方法。但是我們看到scheduleTraversals方法中呼叫了mChoreographer.postCallback方法,這裡應該是關鍵

Choreoprapher類的作用是編排輸入事件、動畫事件和繪製事件的執行,它的postCallback方法的作用就是將要執行的事件放入內部的一個佇列中,最後會執行傳入的Runnable,這裡也就是mTraversalRunnable。所以我們來看看mTraversalRunnable

//ViewRootImpl.class
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            //找到我們的performTraversals方法來,這裡就是開始繪製View的地方啦!
            performTraversals();

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

複製程式碼

當我們呼叫View的invalidate方法後,View會去不斷向上呼叫父佈局的繪製方法並在這個過程中計算需要重繪的區域,最終呼叫過程會走到ViewRootImpl中,呼叫的是ViewRootImpl的performTraversals執行重繪操作。

總結

  • 我們在自定義View時,當需要重新整理View時,如果是在UI執行緒中,那就直接呼叫invalidate方法,如果是在非UI執行緒中,那就通過postInvalidate方法來重新整理View
  • postInvalidate方法實現了訊息機制,最終呼叫的也是invalidate方法來重新整理View
  • invalidate方法最終呼叫的是ViewRootImpl中的performTraversals來重新繪製View

歡迎關注我的微信公眾號,和我一起每天進步一點點!

AntDream

相關文章