Android系統原始碼分析--View繪製流程之-inflate

墨香發表於2018-11-25

上一章我們分析了Activity啟動的時候呼叫setContentView載入佈局的過程,但是分析過程中我們留了兩個懸念,一個是將資原始檔中的layout中xml佈局檔案通過inflate載入到Activity中的過程,另一個是開始測量、佈局和繪製的過程,第二個我們放到measure過程中分析,這一篇先分析第一個inflate過程。

  • Android系統原始碼分析--View繪製流程之-setContentView
  • Android系統原始碼分析--View繪製流程之-inflate
  • Android系統原始碼分析--View繪製流程之-onMeasure
  • Android系統原始碼分析--View繪製流程之-onLayout
  • Android系統原始碼分析--View繪製流程之-onDraw
  • Android系統原始碼分析--View繪製流程之-硬體加速
  • Android系統原始碼分析--View繪製流程之-addView
  • Android系統原始碼分析--View繪製流程之-彈性效果

LayoutInflater.inflate方法基本上每個開發者都用過,也有很多開發者瞭解過它的兩個方法的區別,也有一些開發者去研究過原始碼,我這裡再重複分析這個方法的原始碼其實一是做個記錄,二是指出我認為的幾個重點,幫助我們沒有看過原始碼的人去了解將xml佈局載入到程式碼中的過程。這裡我們需要重點關注三個問題,然後根據對原始碼的分析來解決這三個問題,幫助我們詳細瞭解inflate的過程及影響,那麼這篇文章的目的就達到了。

問題:

  • LayoutInflater.inflate兩個個方法是什麼?
  • 這兩個方法會給我們的檢視顯示帶來什麼影響?
  • View檢視的寬、高是什麼時候解析的?

第一個問題:LayoutInflater.inflate兩個個方法是什麼?

這個問題是最簡單的,基本上這兩個方法都使用過,但是使用的結果卻是不一樣的。下面我貼出來這兩個方法的程式碼:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
    
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
複製程式碼

雖然是兩個方法,但是第一個方法最終會呼叫第二個方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}
複製程式碼

呼叫第二個方法的時候第三個引數是與第二個引數ViewGroup是否為空有關的,這個引數具體作用我們後面程式碼流程分析再說。我們先看使用的幾種情況:

// 第一種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView);

// 第二種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null);

// 第三種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView, false);

// 第四種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView, true);

// 第五種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null, false);

// 第六種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null, true);
複製程式碼

這裡羅列了所有用法,但是不同的用法可能對我們的顯示效果是有影響的,那麼就到了第二個問題,下面通過分析程式碼過程來看看到底有什麼影響。還有第三個問題,是我之前面試的時候被問到的,之前看inflate原始碼沒有很詳細,所以沒有回答上來,這次也一起分析一下,這個寬、高可能很多人覺得是和其他屬性一起解析的,其實不是,這個是單獨解析的,就是因為View的寬、高是單獨解析的,所以會有一些問題出現,可能有些開發者也遇到這個坑,通過這篇文章分析你會的到答案,並且可以準確填上你的坑。

在上面六種情況中是有一樣的:

  • 如果mParentView不是null,那麼:1、4是一樣的,2、5是一樣的,3是一樣,6是一樣,
  • 如果mParentView是null,那麼:1、2、3、5是一樣,4、6是一樣的。

程式碼流程

先看一張流程圖:

Android系統原始碼分析--View繪製流程之-inflate

1.LayoutInflater.inflate

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
複製程式碼

前面提到了inflate方法呼叫最終呼叫到第二個是三個引數的方法,只不過第三個引數是與第二個引數有關係的,這個關係就是root是不是null,如果不是null,傳遞true,反之傳遞false。

2.LayoutInflater.inflate

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            View result = root;

            try {
                int type;
                ...
                final String name = parser.getName();
                ...
                // 要載入的佈局根標籤是merge,那麼必須傳遞ViewGroup進來,並且要新增到該ViewGroup上
                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 {// 根標籤不是merge
                    // temp是要解析的xml佈局中的根佈局檢視
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    // 1.root不為空會解析寬、高屬性(如果不新增的話,那麼會將屬性設定給xml的根佈局)
                    if (root != null) {
                        // root存在才會解析xml根佈局的寬高(如果xml檔案中設定的話)
                        params = root.generateLayoutParams(attrs);
                        // 不將該xml佈局新增到root上的話
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    // 遞迴解析temp(xml檔案中的根佈局)下所有檢視,並按樹形結構新增到temp中
                    rInflateChildren(parser, temp, attrs, true);
                    // 2.root檢視不為空,並且需要新增到root上面,那麼呼叫addView方法並且設定LayoutParams屬性
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    // 3.root為空或者attachToRoot為false,那麼就會將該xml的根佈局賦值給result返回,
                    //   但是root為空時是沒有設定寬高的
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } catch (XmlPullParserException e) {
                ...
            }
            return result;
        }
    }
複製程式碼

這裡開始layout佈局的最開始解析,首先if語句是判斷根檢視,也就是最外層檢視是merge標籤的時候,必須傳入的root不是null,並且第三個引數attachToRoot必須是true,否則丟擲異常。如果root不為null,並且attachToRoot==true,那麼呼叫rInflate方法繼續解析。如果不是merge標籤,那麼解析過程由外向內開始解析,所以首先解析最外層的根檢視並儲存為temp,這裡如果root不是null,那麼就要獲取LayoutParam屬性,這個方法下面再看,然後判斷如果attachToRoot是false的話那麼就給temp設定屬性,如果為true就沒有設定。然後呼叫rInflateChildren方法遞迴解析temp下面的所有檢視,並按樹形結果新增到temp中。接著判斷root不為null,並且attachToRoot為true,那麼將temp新增到root中並且設定屬性值,所以這裡可以看出,attachToRoot引數是是否將解析出來的layout佈局新增到root上面,如果新增則會有屬性值。

**所以這裡的重點就是root決定layout佈局是否被設定ViewGroup.LayoutParams屬性,而attachToRoot決定解析出來的檢視是否新增到root上面。**這裡我們先看獲取的ViewGroup.LayoutParams屬性包含了那幾個屬性值。

3.ViewGroup.generateLayoutParams

    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }
複製程式碼

這裡只是new了一個新物件LayoutParams,我們看看這個LayoutParams物件的建構函式做了什麼

        public LayoutParams(Context c, AttributeSet attrs) {
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_Layout_layout_width,
                    R.styleable.ViewGroup_Layout_layout_height);
            a.recycle();
        }
複製程式碼

這裡呼叫setBaseAttributes函式:

        protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
            width = a.getLayoutDimension(widthAttr, "layout_width");
            height = a.getLayoutDimension(heightAttr, "layout_height");
        }
複製程式碼

到這裡基本明確了,這裡就是獲取檢視的寬、高屬性值的,也就是我們layout佈局中檢視的寬、高值。寬、高包括以下幾種:

public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
複製程式碼

只有具體值,也就是我們設定的layout_width和layout_height值,其實上面第一種已經被第二個取代了。

所以我們這裡看到了檢視的寬、高就是通過ViewGroup.generateLayoutParams來獲取的,如果沒有呼叫那麼解析的檢視就沒有有效的寬、高,如果需要具體值就要自己手動設定了。也就是在呼叫LayoutInflater.inflate方法的時候想讓自己設定的寬、高有效,傳入root就不能是null,否則不會獲取有效的寬、高引數,在後面顯示檢視的時候系統會配置預設的寬、高,而不是我們設定的寬、搞。這個後面會再分析。

還有一種情況就是我想獲取寬、高,但是不想新增到root上,而是我手動新增到別的ViewGroup上面需要怎麼辦,那就是呼叫三個引數的inflate方法,root引數不是null,attachToRoot設定為false就可以了

4.LayoutInflater.rInflate

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();
            
            if (TAG_REQUEST_FOCUS.equals(name)) {   // requestFocus
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {      // tag
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {  // include
                if (parser.getDepth() == 0) {// include不能是根標籤
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {    // merge
                // merge必須是根標籤
                throw new InflateException("<merge /> must be the root element");
            } else {// 正常View
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                // 解析寬高屬性
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                // 遞迴解析
                rInflateChildren(parser, view, attrs, true);
                // parent下的所有view解析完成就會新增到parent上
                viewGroup.addView(view, params);
            }
        }

        // parent下所有檢視解析並add完成就會呼叫onFinishInflate方法,所以我們可以根據這個方法判斷是否解析完成
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }
複製程式碼

上面第2步中,如果根標籤是merge那麼直接呼叫這個方法繼續解析下一層,這裡有五種情況,前兩種我們不分析,基本不用,我們分析下面我們常用的:如果是include標籤,那麼就要判斷include的層級,如果include下沒有其他層級,那麼會丟擲異常,也就是include下必須有layout佈局,然後會呼叫parseInclude來解析include標籤的佈局檔案;另外就是merge巢狀merge也是不行的,會丟擲異常;最後就是正常檢視,通過createViewFromTag來建立該檢視,然後解析寬、高,這裡是直接解析了,只有最外層是要判斷root的,然後呼叫rInflateChildren,這裡rInflateChildren還是會呼叫這裡的方法,也就是形成遞迴解析下一層檢視並新增到外面一層檢視上面,這裡都是有寬、高屬性的。最後有一個if語句,這裡的意思是每個ViewGroup下面的所有層級的檢視解析完成後,會呼叫這個ViewGroup的onFinishInflate方法,通知檢視解析並新增完成,所以我們在自定義ViewGroup的時候可以通過這個方法來判斷你自定義的ViewGroup是否載入完成。

下面我們再看parseInclude方法是如何解析include標籤檢視的

5.LayoutInflater.parseInclude

    private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;

        // include標籤必須在ViewGroup使用,所以這裡parent必須是ViewGroup
        if (parent instanceof ViewGroup) {
            ...

            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                throw new InflateException("You must specify a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            } else {// include中layout的指向id必須有效
                ...

                try {
                    ...

                    final String childName = childParser.getName();

                    if (TAG_MERGE.equals(childName)) {// merge
                        // The <merge> tag doesn't support android:theme, so
                        // nothing special to do here.
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {// 正常View
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;

                        ...
                        ViewGroup.LayoutParams params = null;
                        try {
                            // include是否設定了寬高
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        // 如果include沒有設定寬高,則獲取layout指向的佈局中的寬高
                        if (params == null) {
                            params = group.generateLayoutParams(childAttrs);
                        }
                        view.setLayoutParams(params);

                        // Inflate all children.
                        rInflateChildren(childParser, view, childAttrs, true);

                        ...

                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {// include必須在ViewGroup中使用
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }

        ...
    }
複製程式碼

這裡首先判斷include標籤的上一個層級是不是ViewGroup,如果不是那麼丟擲異常,也就是include必須在ViewGroup內使用。如果是在ViewGroup中使用,那麼接著判斷layout的id是否有效的,如果不是,那麼就要丟擲異常,也就是include必須包含有效的檢視佈局,然後開始解析layout部分檢視,如果跟佈局是merge,那麼呼叫解析對應merge的方法rInflate,也就是步驟4,如果是正常的View檢視,那麼通過createViewFromTag方法獲取檢視,然後獲取include標籤的寬、高,如果include中沒有設定才獲取include包含的layout中的寬、高,也就是include設定的寬、高優先於layout指向的佈局中的寬、高,所以這裡要注意了。獲取完成會設定對應的寬高屬性,然後呼叫rInflateChildren遞迴完成layout下所有層級檢視的載入。基本的邏輯就差不多了,其實並不複雜,還有個方法需要簡單介紹下-createViewFromTag,根據xml中的標籤也就是檢視的名字載入View實體。

6.LayoutInflater.createViewFromTag

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ...
        try {
            View view;
            ...

            if (view == null) {
                ...
                try {
                    // 系統自帶的View(直接使用名字,不用帶包名,所以沒有".")
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {// 帶有包名的View(例如自定義的View,或者引用的support包中的View)
                        view = createView(name, null, attrs);
                    }
                } finally {
                    ...
                }
            }

            return view;
        } catch (InflateException e) {
            ...
        }
    }
複製程式碼

這個方法裡有兩行註釋,我解釋一下,我們在xml佈局中有兩種寫法,一種是系統自帶的檢視,例如:FrameLayout,LinearLayout等,一種是自定義的或者是Support包中的也就是帶有包名的檢視:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@+id/header_rl"
            android:scrollbars="vertical"/>

    <ProgressBar
            android:id="@+id/progress"
            android:layout_centerInParent="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
</RelativeLayout>
複製程式碼

上面這個佈局就是包含兩種,系統自帶的就是ProgressBar,還有就是帶有包名的,這兩種解析方法是有區別的。系統自帶的用onCreateView方法建立View,帶有包名的通過createView方法建立。我們先看第一個:

7.LayoutInflater.onCreateView

    protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        // 系統正常View要新增字首,比如:LinearLayout,新增完字首就是android.view.LinearLayout
        return createView(name, "android.view.", attrs);
    }
複製程式碼

系統的檢視都在android.view包下,所以要新增字首“android.view.”,新增完也是完整的檢視名稱,就和自定義的是一樣的,最終還是呼叫createView方法:

8.LayoutInflater.createView

    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        ...
        Class<? extends View> clazz = null;

        try {

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                ...
                constructor = clazz.getConstructor(mConstructorSignature);
                ...
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        ...
                        constructor = clazz.getConstructor(mConstructorSignature);
                        ...
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        ...
                    }
                }
            }

            ...

            final View view = constructor.newInstance(args);
            ...
            return view;

        } catch (NoSuchMethodException e) {
            ...
        }
    }
複製程式碼

這裡就很簡單了就是根據完整的路徑名稱載入出對應的Class檔案,然後建立對應的Constructor檔案,通過呼叫Constructor.newInstance建立對應的View物件,這就是將xml檔案解析成java物件的過程。

總結

LayoutInflate.inflate方法很重要,這是我們將xml佈局解析成java物件的必須過程,所以掌握這個方法的原理非常重要,上面分析的時候也提出一些重點的內容,所以我們再總結一下,方便記憶:

  • inflate方法的第二個引數root不為null,載入xml檔案時根檢視才有具體寬、高屬性;
  • inflate方法的第三個引數attachToRoot是true時,解析的xml佈局會被新增到root上,反之不新增;
  • 呼叫兩個引數的inflate方法時,引數attachToRoot = (root != null);
  • include設定的寬、高優先於layout指向的佈局中設定的寬、高;
  • include不能是根標籤;
  • merge必須是根標籤
  • include必須有有效的layout id

程式碼地址:

直接拉取匯入開發工具(Intellij idea或者Android studio)

gitlab.com/yuchuangu85…

注:

首發地址:www.codemx.cn

Android開發群:192508518

微信公眾賬號:Code-MX

Android系統原始碼分析--View繪製流程之-inflate

注:本文原創,轉載請註明出處,多謝。

相關文章