1. 程式人生 > >由GridView(RecyclerView、ListView)首項重複繪製引起的探究

由GridView(RecyclerView、ListView)首項重複繪製引起的探究

如果Item佈局包含非同步載入的ImageView,每次ImageView顯示都會請求介面重繪,此時仍會呼叫N次OnMeasure方法。

每次呼叫GridView的OnMeasure方法都會呼叫Adapter的getView方法一次,其中position = 0。

父檢視可能在它的子檢視上呼叫一次以上的measure(int,int )方法。 例如,父檢視可以使用unspecified dimensions來將它的每個子檢視都測量一次來算出它們到底需要多大尺寸,如果所有這些子檢視沒被限制的尺寸的和太大或太小,那麼它會用精確數值再次呼叫measure()(也就是說,如果子檢視不滿意它們獲得的區域大小,那麼父檢視將會干涉並設定第二次測量規則)。

我們都知道一個佈局要顯示螢幕要經過測量(onMeasure)、佈局(onLayout)、繪製(onDraw)這三個步驟。但是究竟是誰規定的這三個步驟的定義和順序?在什麼條件下會引起佈局重繪呢?

看完ViewRootImpl這個類就恍然大悟了。

The top of a view hierarchy, implementing the needed protocol between View and the WindowManager. This is for the most part an internal implementation detail of {@link WindowManagerGlobal}.

這是ViewRootImpl類的註釋。
ViewRootImpl是Android中檢視層級的頂層,同時實現了View和WindowManager之間必要的協議,是兩者之間的橋樑。ViewRootImpl是WindowManagerGlobal大部分內部細節的實現。

一、Activity建立
我們知道,每當通過Intent啟動一個ActivityA時,這其實是一個程序間通訊的過程,基本都是ActivityThread類和遠端的ActivityManagerService進行交流。通過Binder機制,告訴服務程序ActivityManger要啟動的ActivityA的資訊,ActivityManager會查詢已經註冊過的Activity列表中有沒有這個將要啟動的ActivityA。如果找到的話,會新建一個ActivityA的物件,並回調ActivityA生命週期方法。這個Activity就展示到我們眼前了。

二、建立Window
每建立一個Activity,都會對應建立一個PhoneWindow物件,由它來建立DecorView和ContentView。我們在Activity的OnCreate方法中設定的佈局,也是通過PhoneWindow處理的。
PhoneWindow的建立時機是在Activity中的attach方法中的。

 final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
        //這裡建立了PhoneWindow物件
        mWindow = new PhoneWindow(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

三、將View新增到Window中
PhoneWindow建立了頁面檢視樹種最頂層的View和ViewGroup,包括:DecorView、最外層佈局mContentRoot、內容佈局父佈局mContentParent

//建立DecorView
protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }
//建立mContentRoot
View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

//mContentParent其實是mContentRoot的一部分
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

那是在什麼時候把DecorView新增到Window上的呢?
是在onResume的時候
我們先看一個ActivityThread中handleResumeActivity方法。

//這是Activity第一次可見的時候走的邏輯。
//此時Activity所屬的Window還沒有新增到WindowManager中
if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    //這裡將decorview新增到了windowManager中。WindowManager是一個介面,它有一個實現類WindowManagerImpl,但是addView方法其實是委託給了WindowManagerGlobal去實現的。
                    wm.addView(decor, l);
                }

上面是Activity第一OnResume的時候,當Activity從暫停狀態恢復時是怎麼把View新增到Window上的呢?還是看handleResumeActivity方法。

//這部分就是處理Activity由於頁面更新而發生的變化
if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                if (r.newConfig != null) {
                    r.tmpConfig.setTo(r.newConfig);
                    if (r.overrideConfig != null) {
                        r.tmpConfig.updateFrom(r.overrideConfig);
                    }
                    if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
                            + r.activityInfo.name + " with newConfig " + r.tmpConfig);
//Activity配置變化引起的介面更新                    performConfigurationChanged(r.activity, r.tmpConfig);
                    freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
                    r.newConfig = null;
                }
                if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
                        + isForward);
                WindowManager.LayoutParams l = r.window.getAttributes();
                //由於軟鍵盤變化引起的介面更新
                if ((l.softInputMode
                        & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                        != forwardBit) {
                    l.softInputMode = (l.softInputMode
                            & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                            | forwardBit;
                    if (r.activity.mVisibleFromClient) {
                        ViewManager wm = a.getWindowManager();
                        View decor = r.window.getDecorView();
                        wm.updateViewLayout(decor, l);
                    }
                }
                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                //上述變化都會通過Activity的makeVisible方法,將更改後View新增到Widow中。
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
//Activity的makeVisible()方法,可見在這個方法再次呼叫了windowmanager的addView方法
void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

四、addView的細節
上面說到在Activity中onResume時會通過WindowManager將準備好的View新增到Window中,

WindowManagerImpl中的addView方法,委託給了WindowManagerGlobal的addView方法
@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
//WindowManagerGlobal的addView方法
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            ...
            ...
             // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }

            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            //最終呼叫了ViewRootImpl的setView方法
             root.setView(view, wparams, panelParentView);
//ViewRootImpl的setView方法,
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
            //將view賦值給全域性變數mView
                mView = view;

                mAttachInfo.mDisplayState = mDisplay.getState();
                mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);

                mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                mFallbackEventHandler.setView(view);
                mWindowAttributes.copyFrom(attrs);
                if (mWindowAttributes.packageName == null) {
                    mWindowAttributes.packageName = mBasePackageName;
                }
                ...
                程式碼省略
                ...
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                //這裡呼叫了requestLayout()方法
                requestLayout();
//ViewRootImpl的requestLayout()方法
@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //首先檢查當前執行緒是否是主執行緒
            checkThread();
            mLayoutRequested = true;
            //開始安排遍歷
            scheduleTraversals();
        }
    }
void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //使用choreographer傳送非同步訊息mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

最終會走到PerformTraversals(),這是一個巨長的方法,除了會走performMeasure、performLayout、performDraw方法外,還會有其他的一些非常重要的邏輯判斷。今天我們只探究上述方法的呼叫,所以就不再討論額外的邏輯了。

private void performTraversals() {
    ...
    if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                    if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed!  mWidth="
                            + mWidth + " measuredWidth=" + host.getMeasuredWidth()
                            + " mHeight=" + mHeight
                            + " measuredHeight=" + host.getMeasuredHeight()
                            + " coveredInsetsChanged=" + contentInsetsChanged);

                     // Ask host how big it wants to be
                    //這裡呼叫了performMeasure方法
                    //然後會呼叫mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);方法                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(TAG,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }
        ....

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
        //這裡執行了performLayout方法
        //然後會呼叫View的layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())方法
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);

            // By this point all views have been sized and positioned
            // We can compute the transparent area

    ......

    if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }
                //這裡執行了perfromDraw方法,然後會呼叫View的mView.draw(canvas);方法
                performDraw();
            }

至此,我們終於知道onMeasure、onLayout、onDraw這個三種繪製流程中關鍵方法的呼叫順序是什麼地方定義的了。
當然,在繪製過程中,這三個方法並不是簡單的呼叫一次,從原始碼中可以看出,不同的條件情況下這三個方法會重複呼叫。