Android系統源碼分析--View繪制流程之-inflate

上一章我們分析了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是一樣的。

代碼流程

先看一張流程圖:

inflate.jpg

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)

Android_Framework_Source

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

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

推薦閱讀更多精彩內容