從requestLayout()初探View的繪制原理

在自定義View時,涉及到View的大小變化時,通常會涉及到一個函數requestLayout(),字面意思大家都知道是要求重新執行View的繪制中的layout,但是requestLayout()是如何做到讓View重新繪制的呢?只是簡單調用本身的layout(...)方法么?

    public void requestLayout() {
        ...
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        ....
    }

可以看到,View類的requestLayout()是調用ViewParentrequestLayout()方法,但是ViewParent類只是一個接口沒有實現,所以我們去找mParent的引用。

我們逐級打印出mParent,會發現最后指向了PhoneWindow$DecorView,也就是我們常說的根View。但是再看下去,發現DecorView其實是FrameLayout的子類,也就是說又指回了View,這下似乎陷入了循環……

mParent也只有在assignParent方法中實現了賦值,但View類本身并沒有調用這個方法

void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"
                    + " it already has a parent");
        }
    }

那么我們只能從DecorView生成的地方來考慮了,眾所周知DecorView作為Activity的根View,那么肯定和Activity生成的地方有關。我們從Activity的創建開始找,最后在ActivityThread類中的handleResumeActivity()方法中,找到對Activity中DecorView的賦值

 final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
        ...
        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;
                    wm.addView(decor, l);
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            }
        ...
        }

getDecorView()Window類下的一個抽象方法,而Android中實現了Window的類只有PhoneWindow,于是我們去PhoneWindow中找

這里說明一點,mDecor的創建時間并不是調用getDecorView()時,而是ActivityonCreate方法,因為ActivityThread先調用了performLaunchActivity方法創建了activity并執行onCreate方法,而onCreate方法中的setContentView會調用PhoneWindowsetContentView方法,這里最先執行了installDecor()

@Override
    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
    }
    
   private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        
  protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
  }

好像還是沒有線索……但是我們發現在handleResumeActivity方法中DecorView的實例最后被一個ViewManager的子類WindowManager通過addView()添加進去了,會不會跟這個WindowManager有關系呢?

//handleResumeActivity中指向的方法,在Activity.java中
public WindowManager getWindowManager() {
        return mWindowManager;
    }

指向了Activity中的一個變量mWindowManager,其在Activityattach方法中被初始化

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) {
            ...
            mWindowManager = mWindow.getWindowManager();
            ...
           }

調用到Window類中的方法

/**
     * Return the window manager allowing this Window to display its own
     * windows.
     *
     * @return WindowManager The ViewManager.
     */
    public WindowManager getWindowManager() {
        return mWindowManager;
    }

再去找mWindowManager賦值的地方

 public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

終于發現了一個不一樣的類WindowManagerImpl,這應該就是實現了WindowManager接口的類,看下它的addView()方法

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

又扔給了一個mGlobal變量,這個變量屬于WindowManagerGlobal類,可以看下這個類的介紹

**
 * Provides low-level communication with the system window manager for
 * operations that are not associated with any particular context.
 *
 * This class is only used internally to implement global functions where
 * the caller already knows the display and relevant compatibility information
 * for the operation.  For most purposes, you should use {@link WindowManager} instead
 * since it is bound to a context.

呃……英文不好自行找詞典,大概意思就是提供了一個全局變量來實現底層系統與表現層的溝通,再來看他的addView()方法

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
         ViewRootImpl root;
         synchronized (mLock) {
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
}

可以看到,這里最關鍵的操作就是初始化了一個ViewRootImpl類的對象,調用它的setView方法注入之前的View。

那么ViewRootImpl類是什么?它的setView方法又做了什么呢?

我們先來看一下ViewRootImlp類的Javadoc

* 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}.
 public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks
        
//View階層的頂級,實現了View和WindowManager之間需要的協議,這是WindowManagerGlobal

來研究一下它的setView方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if(mView ==null){
                 //省略了部分無關代碼
                 mView = view;
                requestLayout();
                ...
                view.assignParent(this); 
            }
        }
}

可以看到,setView方法基本只有在第一次調用時才會生效,它先調用了自己的requestLayout()方法,然后又調用ViewassignParent(ViewParent v)方法把自己注入View中

這下就可以確定View最上層的mParent變量,最終指向的是ViewParentImpl類的實例引用

那讓我們再來看看ViewParentImpl中如何實現requestLayout()方法的

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread(); //這里是Android中為什么不能用子線程修改ui的關鍵
            mLayoutRequested = true; //修改標記為true
            scheduleTraversals();
        }
    }

眾所周知,Android中是不能用子線程來更新ui的,原因就是在于ViewRootImpl類中的requestLayout方法在執行時,會首先檢查當前線程是否是ViewRootImpl實例創建的線程。而唯一的ViewRootImpl對象是在主線程中被系統創建的。所以子線程中更新ui,就會導致checkThread()方法驗證失敗而拋出異常

除了checkThread()以外,還執行了scheduleTraversals()方法

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這里執行了Choreographer類的實例的postCallback方法,方法最后將這個Runnable放入Message中發出到MessageQueue中并執行,我們來看這個Runnable做了什么

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

再看看doTraversal()

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

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

            performTraversals();

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

其中最關鍵的是執行了performTraversals()方法,這也是View繪制中最關鍵的一個方法

private void performTraversals() {
     //省略了部分無關代碼
     final View host = mView;
     ...
     host.dispatchAttachedToWindow(mAttachInfo, 0);//注入AttachInfo
     ...
     boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
     if (layoutRequested) {
            mLayoutRequested = false;   //清除標記
     }
     ...
     if (!mStopped || mReportNextDraw) {
            ...
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
     }
     ...
     final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
     if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
     }
     ...
     boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() ||
                viewVisibility != View.VISIBLE;
        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                ...
                performDraw();
            }
        }
     ...
}

其中performMeasureperformLayoutperformDraw最后基本都是去執行了View的measure、layout、draw方法,這也是View繪制三大步驟調用的時間點。

這里我還特地標注一下host.dispatchAttachedToWindow(mAttachInfo, 0)方法,這個方法給View中注入了mAttachInfo的引用,而mAttachInfo的初始化是在ViewRootImpl的構造函數里

public ViewRootImpl(Context context, Display display) {
    ...
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
    ...
}

AttachInfo這個類基本就是提供各種底層信息的,借此,Window和View的聯系就建立起來了。

最后上一張圖來整理下整個調用鏈

requestLayout

最后歡迎關注我的GithubBlog

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容