1. 程式人生 > >你需要知道的Android View的建立

你需要知道的Android View的建立

View的建立與繪製一向是很多人望而止步的問題。然而我們在平常的應用開發中是最經常運用到的setContentView(),我們都會用在Activity的onCreate()的時候呼叫setContentView()來載入編輯好的XML佈局。但是實際上建立與繪製一個View,內部的實現方式的確比我們表面所編寫的程式碼複雜得多,導致大家沒能深入去了解View的建立與繪製。接下來我們一步步來了解View的建立與繪製。

在研究setContentView()方法前,我們首先先看一下這圖:

這裡寫圖片描述

上圖中,DecorView是個應用視窗的最頂層View。(Decor的英文全稱是Decoration,即“修飾”的意思)。DecorView只有一個子元素是垂直LinearLayout。在LinearLayout下有兩個子佈局,第一個是ViewStub,ViewStub就是ActionBar,它會根據theme判斷有沒使用ActionBar來決定是否引入ActionBar佈局。第二個是FrameLayout,這就是我們應用真實使用的父佈局。

大家可以通過sdk工具Hierarchy Viewer來檢視驗證一下ViewTree的情況。

這裡寫圖片描述

Window類 位於/frameworks/base/core/java/Android/view/Window.java

Window,中文解析“視窗”。它是一個巨集觀的概念。該類是一個抽象類,提供了繪製視窗的通用API,我們可以理解為它是一個載體。

接著我們看一下PhoneWindow,它是Android中Window的具體實現類。

PhoneWindow位於/frameworks/policies/base/phone/com/android/internal/policy/impl/PhoneWindow.java

PhoneWindow繼承於Window類,我們可以通過實現具體抽象方法去繪製視窗,該類還包含DecorView內部類。我們平時呼叫的setContentView()方法設定Activity的使用者介面時,實際上是對PhoneWindow的ViewTree的設定。

我們通過一個比喻來理解他們之間的關係。Window類相當於一幅畫(抽象概念,什麼畫我們未知) ,PhoneWindow為一副齊白石先生的山水畫(具體概念,我們知道了是誰的、什麼性質的畫),DecorView則為該山水畫的具體內容(有山、有水、有樹,各種介面)。DecorView呈現在PhoneWindow上。好了,有了這部分的認識之後,我們就開始從原始碼的角度去認識View的建立。

當我們自定義Activity繼承Android.app.Activity時候,呼叫的setContentView()如下:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

getWindow()方法返回一個PhoneWindow物件,那就是說呼叫的是PhoneWindow的setContentView()方法。原始碼如下:

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) { 
        // mContentParent即為上面提到的ContentView的父容器,若為空則呼叫installDecor()生成
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // mContentParent不為null,則移除decorView的所有子View
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 一般情況會來到這裡,呼叫mLayoutInflater.inflate()方法來填充佈局
        // 填充佈局也就是把我們設定的ContentView加入到mContentParent中
        mLayoutInflater.inflate(layoutResID, mContentParent); // 2
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 呼叫onContentChanged()回撥方法通知Activity視窗內容發生了改變
        cb.onContentChanged();
    }
}

首先判斷了mContentParent是否為null,如果為空則執行installDecor()方法,那麼這個mContentParent又是什麼呢?我們看一下它的註釋。

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;

它是一個ViewGroup型別,結合2處程式碼,可以得知,這個mContentParent是我們設定的佈局的父佈局。

梳理下:Activity通過PhoneWindow的setContentView方法來設定佈局,而設定佈局之前,會先判斷是否存在mContentParent,而我們設定的佈局檔案則是mContentParent的子元素。

接著我們看一下installDecor(),我們看一下PhoneWindow#installDecor:

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(); // 1
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor); // 2
        ...
        } 
    }
}

首先,會執行1處程式碼,呼叫PhoneWindow#generateDecor方法:

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

可以看出,這裡例項化了DecorView,而DecorView則是PhoneWindow類的一個內部類,繼承於FrameLayout,由此可知它也是一個ViewGroup。DecorView上面我們已經對它進行過一次解析了,大家可以返回去回顧下。接下來我們看2處PhoneWindow#generateLayout方法的程式碼:

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        // 從主題檔案中獲取樣式資訊
        // 載入TitleBar方法一
        TypedArray a = getWindowStyle();

        ...

        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }

        if(...){
            ...
        }

        // Inflate the window decor.
        // 載入窗口布局
        int layoutResource;
        // 載入TitleBar方法二
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } else if(...){
            ...
        }

        View in = mLayoutInflater.inflate(layoutResource, null);    //載入layoutResource
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //往DecorView中新增子View,即mContentParent
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 這裡獲取的就是mContentParent
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        ...

        return contentParent;
    }

由以上程式碼可以看出,該方法還是做了相當多的工作的,首先根據設定的主題樣式來設定DecorView的風格,比如說有沒有titlebar之類的,就是解析我們為Activity設定theme的地方,至於Theme的設定:
1.我們可以在AndroidManifest裡面進行設定,為我們的Activity配置相應屬性,即android:theme=”“,PhoneWindow物件呼叫getWindowStyle()方法獲取值。
2.也可以在setContentView()前呼叫requestFeature,指定requestFeature()指定視窗修飾符,PhoneWindow物件呼叫getLocalFeature()方法獲取值;

對Theme操作完,我們才對layoutResource賦值的,因此我相信有不少人都曾經遇到一個錯誤——“requestFeature() must be called before adding content”。

接著通過對features和mIsFloating的判斷,設定視窗的風格修飾為layoutResource進行賦值。得到了layoutResource以後,通過LayoutInflater.inflate()方法生成View物件。並加入到decor中。這就是為DecorView新增子View,而這裡的子View則是上面提到的mContentParent。如果上面設定了FEATURE_NO_ACTIONBAR,那麼DecorView就只有mContentParent一個子View,這也解釋了mContentParent物件註釋:mContentParent是DecorView本身或者是DecorView的一個子元素。

小結:DecorView是頂級View,內部有titlebar和contentParent兩個子元素,而內部根據theme設定TitleBar,和選擇系統中的佈局檔案,將佈局檔案通過inflate轉化為view,加入到mDecor中;這些佈局檔案中都包含一個id為content的FrameLayout,將其引用返回給mContentParent。

瞭解完PhoneWindow#installDecor後我們接著PhoneWindow#setContentView(),看到那部分2處程式碼:mLayoutInflater.inflate(layoutResID, mContentParent);相信LayoutInflater大家跟setContentView()一樣常用。因為在一些動態載入View和BaseAdapter介面卡的程式碼編寫中我們都會用到。我們來看一下它的程式碼:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

LayoutInflater.inflate()將上面建立的decorView作為root的引數。上面的程式碼比較簡單,重點是return那行的inflate(),我們看一下里面的實現過程:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                int type;
                // 一直讀取xml檔案,直到遇到開始標記
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }
                // 單獨處理<merge>標籤
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    // 遞迴地填充佈局
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    // 能在XML發現根View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        // 獲取父容器的佈局引數(LayoutParams)
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            // 若attachToRoot引數為false,則我們只會將父容器的佈局引數設定給根View
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    // 遞迴載入根View的所有子View
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    // 若父容器不為空且attachToRoot為true,則將父容器作為根View的父View包裹上來
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    // 若root為空或是attachToRoot為false,則以根View作為返回值
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (Exception e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                                + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);

            return result;
        }
    }

在上面的原始碼中,LayoutInflater其實就是使用Android提供的pull解析方式來解析佈局檔案的。首先對於佈局檔案中的標籤進行單獨處理,呼叫rInflate()方法來填充佈局。非標籤情況下會呼叫一個createViewFromTag()方法。從方法名的命名方式我們可以猜到這是通過xml節點來建立View物件的。

無論是標籤還是非標籤,它們實際都是通過xml節點來建立View物件,並新增到父佈局中。

到這來setContentView()的整體執行流程我們就分析完了,至此我們已經完成了Activity的ContentView的建立與設定工作。但是我們的View還是不可見的,因為我們只是做了建立和設定載入而已。接下來就到View的繪製流程步驟。但是在測量、佈局、繪製工作前還有一個步驟,那就是把DecorView新增至Window中。

每一個Activity元件都有一個關聯的Window物件,用來描述一個應用程式視窗。每一個應用程式視窗內部又包含有一個View物件,用來描述應用程式視窗的檢視。上文分析了建立DecorView的過程,現在則要把DecorView新增到Window物件中。而要了解這個過程,我們首先要簡單先了解一下Activity的建立過程:

首先,在ActivityThread#handleLaunchActivity中啟動Activity,在這裡面會呼叫到Activity#onCreate方法,從而完成上面所述的DecorView建立動作,當onCreate()方法執行完畢,在handleLaunchActivity方法會繼續呼叫到ActivityThread#handleResumeActivity方法,我們看看這個方法的原始碼:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { 
    //...
    ActivityClientRecord r = performResumeActivity(token, clearHide); // 這裡會呼叫到onResume()方法

    if (r != null) {
        final Activity a = r.activity;

        //...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow(); // 獲得window物件
            View decor = r.window.getDecorView(); // 獲得DecorView物件
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager(); // 獲得windowManager物件
            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); // 呼叫addView方法
            }
            //...
        }
    }
}

在該方法內部,獲取該activity所關聯的window物件,DecorView物件,以及windowManager物件,而WindowManager是抽象類,它的實現類是WindowManagerImpl,所以後面呼叫的是WindowManagerImpl#addView方法,我們看看原始碼:

public final class WindowManagerImpl implements WindowManager {    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...
    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
}

接著呼叫了mGlobal的成員函式,而mGlobal則是WindowManagerGlobal的一個例項,那麼我們接著看WindowManagerGlobal#addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

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

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView); // 2
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

先看1程式碼處,例項化了ViewRootImpl類,接著,在2處程式碼,呼叫ViewRootImpl#setView方法,並把DecorView作為引數傳遞進去,在這個方法內部,會通過跨程序的方式向WMS(WindowManagerService)發起一個呼叫,從而將DecorView最終新增到Window上。這個過程中ViewRootImpl、DecorView和WMS會彼此關聯,至於詳細過程這裡不展開來說了。

最後通過WMS呼叫ViewRootImpl#performTraverals方法開始View的測量、佈局、繪製流程,這將在下篇繼續分析View的繪製測量。下面一張圖總結我們View的建立的整套流程,大家可以通過下圖來對上面的知識分析進行再次理解。

這裡寫圖片描述