1. 程式人生 > >android inflater詳解

android inflater詳解

簡述: 簡單的說,LayoutInflater就是是一個用來解析xml佈局檔案的類。該篇文章將對LayoutInflater類進行分析,內容包括:

  1. LayoutInflater在哪裡建立
  2. 如何獲取LayoutInflater物件
  3. 檢視的建立過程(xml轉換成View的過程)
  4. inflate方法的兩個重要引數(root、attachToRoot)分析

LayoutInflater的來源: LayoutInflater和其他系統服務一樣,也是在ContextImpl類中進行註冊的,ContextImpl類中有一個靜態程式碼塊,應用程式用到的系統服務都在這進行註冊:

class ContextImpl extends Context { static { // …

    // 註冊ActivityManager服務
    registerService(ACTIVITY_SERVICE, new ServiceFetcher() {
            public Object createService(ContextImpl ctx) {
                return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
            }});
    // 註冊WindowManager服務
    registerService(WINDOW_SERVICE, new ServiceFetcher() {
            Display mDefaultDisplay;
            public Object getService(ContextImpl ctx) {
                Display display = ctx.mDisplay;
                if (display == null) {
                    if (mDefaultDisplay == null) {
                        DisplayManager dm = (DisplayManager)ctx.getOuterContext().
                                getSystemService(Context.DISPLAY_SERVICE);
                        mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
                    }
                    display = mDefaultDisplay;
                }
                return new WindowManagerImpl(display);
            }});

    // ....

    // 註冊LayoutInflater服務
    registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
            public Object createService(ContextImpl ctx) {
                return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
            }});

    // ...其他服務的註冊,不一一列舉,有興趣可以自己看原始碼
}

// ...其他程式碼

// 儲存所有服務的ServiceFetcher集合
private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
        new HashMap<String, ServiceFetcher>();

private static void registerService(String serviceName, ServiceFetcher fetcher) {
    if (!(fetcher instanceof StaticServiceFetcher)) {
        fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
    }
    SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
}

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 從程式碼中可以發現,除了LayoutInflater的註冊,還有我們常見的WindowManager、ActivityManager等的註冊。所有的註冊都呼叫了靜態方法:registerService,這裡所有的服務並不是在靜態程式碼塊中直接建立,而是採用飢渴式方法,只建立了對應服務的獲取器ServiceFetcher物件。在真正使用特定服務的時候才建立,SYSTEM_SERVICE_MAP是一個靜態的集合物件,儲存了所有服務的獲取器(ServiceFetcher)物件,map的鍵是對應服務的名稱。只需要呼叫獲取器(ServiceFetcher)的getService(Context context)方法既可以獲取對應的系統服務。

我們只關注LayoutInflater的獲取器(ServiceFetcher)是如何實現的,其getService(Context context);方法呼叫了com.android.internal.policy.PolicyManager#makeNewLayoutInflater(Context context)

public static LayoutInflater makeNewLayoutInflater(Context context) { return new BridgeInflater(context, RenderAction.getCurrentContext().getProjectCallback()); } 1 2 3 這裡提一下,上面程式碼是android-sdk-21版本的原始碼,建立了一個BridgeInflater物件,如果是android-sdk-19及以下的原始碼,PolicyManager#makeNewLayoutInflater方法應該是:

public static LayoutInflater makeNewLayoutInflater(Context context) { return sPolicy.makeNewLayoutInflater(context); } 1 2 3 接著呼叫了com.android.internal.policy.impl.Policy#makeNewLayoutInflater(Context context)方法:

public LayoutInflater makeNewLayoutInflater(Context context) { return new PhoneLayoutInflater(context); } 1 2 3 也就是說android-sdk-19及以下的版本是建立一個PhoneLayoutInflater物件。

BridgeInflate和PhoneLayoutInflater都是繼承自LayoutInflater,實現瞭解析xml佈局的API,將會在後面分析xml佈局檔案解析過程時用上。這裡不討論兩者的實現以及區別。

獲取LayoutInflater物件: 按照上面的邏輯,LayoutInflater不需要我們自己new,framework層已經幫我們建立好,自然也會也會提供API供開發者獲取LayoutInflater物件。

方式一: 既然LayoutInflater是在ContextImpl中註冊的,Context也提供了介面來獲取LayoutInflater服務,也就是Context#getSystemService(String name);方法:

@Override public Object getSystemService(String name) { ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); return fetcher == null ? null : fetcher.getService(this); } 1 2 3 4 5 該方法從SYSTEM_SERVICE_MAP集合內取出對應服務的獲取器ServiceFetcher,並呼叫其getService方法來獲取服務,首次呼叫的時候,將會呼叫到ServiceFetcher類的createService方法來建立一個LayoutInflater物件,之後將會返回已經建立好的物件。

所有的其他獲取LayoutInflater物件的方式,都將呼叫到Context#getSystemService(String name);方法,我們繼續往下看看其他方式是如何獲取的。

方式二: 通過LayoutInflater#from(context)方法來獲取:

public static LayoutInflater from(Context context) { LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (LayoutInflater == null) { throw new AssertionError(“LayoutInflater not found.”); } return LayoutInflater; } 1 2 3 4 5 6 7 8 最終該方式還是呼叫了方式一中說到的Context#getSystemService(String name);方法,並將LayoutInflater服務名稱傳遞進去。

方式三: 如果在Activity內,可以通過Activity#getLayoutInflater();方法獲取LayoutInflater,該方法是Activity封裝的一個方法:

@NonNull public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); } 1 2 3 4 Activity裡的getWindow返回的是一個PhoneWindow物件,接著看PhoneWindow#getLayoutInflater();

@Override public LayoutInflater getLayoutInflater() { return mLayoutInflater; } 1 2 3 4 返回了一個LayoutInflater物件,其初始化是在PhoneWindow的構造方法裡:

public PhoneWindow(Context context) { super(context); mLayoutInflater = LayoutInflater.from(context); } 1 2 3 4 其最終呼叫了方式二中的LayoutInflater#from(Context context);方法。

佈局解析過程 接著,分析LayoutInflater是如何將一個xml佈局檔案解析成一個View物件的。涉及到以下內容:

LayoutInflater#inflate(…);的四個重構方法 LayoutInflater#inflate(…);是如何解析檢視的 LayoutInflater#inflate(…);的四個重構方法 通過LayoutInflater對外提供的四個inflate重構方法來入手檢視解析流程:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root); public View inflate(XmlPullParser parser, @Nullable ViewGroup root); public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot); public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot); 1 2 3 4 呼叫關係如下:

  1. 第一個重構方法最後呼叫了第三個重構方法,第三個重構方法最後呼叫了第四個重構方法。
  2. 第二個重構方法最終呼叫了第四個重構方法

第一個:

public View inflate(int resource, ViewGroup root) { // 呼叫第三個重構方法 return inflate(resource, root, root != null); } 1 2 3 4 第二個:

public View inflate(XmlPullParser parser, ViewGroup root) { // 呼叫第四個重構方法 return inflate(parser, root, root != null); } 1 2 3 4 第三個:

public View inflate(int resource, ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, “INFLATING from resource: “” + res.getResourceName(resource) + “” (” + Integer.toHexString(resource) + “)”); } // 通過resource資原始檔獲取xml解析器 final XmlResourceParser parser = res.getLayout(resource); try { // 呼叫第四個重構方法 return inflate(parser, root, attachToRoot); } finally { parser.close(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 第四個:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { // 省略內容,後面分析 } 1 2 3 真正開始佈局的解析流程的是第四個重構方法,也就是說我們只要分析第四個重構方法的流程就能知道xml佈局檔案是如何被解析的。

LayoutInflater#inflate(…);是如何解析檢視的 檢視的解析過程可以總結成:

使用XmlPullParser遍歷xml檔案內的所有節點 在遍歷到某一節點時,根據節點名字生成對應的View物件 在生成View物件時,將AttributeSet以及Context傳遞給View物件的構造方法,在構造方法中,View或者其子類將通過AttributeSet獲取自身的屬性列表,並用來初始化View。如background等屬性。 在分析檢視的解析過程之前,需要先了解什麼是XmlPullParser,他是第二個和第四個重構方法的引數,XmlPullParser是一個介面,定義了一系列解析xml檔案的API。

java中解析xml的常用方式有DOM和SAX兩種方式,pull解析是android提供的一種。

這裡引用一段對pull方式的描述:

在android系統中,很多資原始檔中,很多都是xml格式,在android系統中解析這些xml的方式,是使用pul解析器進行解析的,它和sax解析一樣(個人感覺要比sax簡單點),也是採用事件驅動進行解析的,當pull解析器,開始解析之後,我們可以呼叫它的next()方法,來獲取下一個解析事件(就是開始文件,結束文件,開始標籤,結束標籤),當處於某個元素時可以呼叫XmlPullParser的getAttributte()方法來獲取屬性的值,也可呼叫它的nextText()獲取本節點的值。

對xml解析方式的使用有興趣可以參閱: android解析XML總結(SAX、Pull、Dom三種方式)

那麼XmlPullParser物件是如何生成的。看看重構方法三:

final XmlResourceParser parser = res.getLayout(resource); 1 res是Resource類物件,resource是資原始檔id,看看Resource#getLayout(int id);方法的實現:

public XmlResourceParser getLayout(int id) throws NotFoundException { return loadXmlResourceParser(id, “layout”); } 1 2 3 Resource#loadXmlResourceParser(int id, String type);方法最終將會返回一個XmlBlock#Parser型別的物件:

final class XmlBlock { // … final class Parser implements XmlResourceParser { // … } // … } 1 2 3 4 5 6 7 XmlResourceParser繼承自XmlPullParser、AttributeSet以及AutoCloseable(一個定義了不使用時需要關閉的介面):

public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {

public void close();

} 1 2 3 4 也就是說最終返回了一個XmlPullParser介面的實現類Parser,Parser類還實現了AttributeSet介面。

那麼大家經常在View的構造方法裡見到的AttributeSet到底什麼:

Android引入了pull解析,其中XmlPullParser這個介面定義了操作pull解析方式對xml檔案的所有操作介面,包括對節點的操作,對節點內的屬性的操作,以及next等介面。而AttributeSet則是Android針對資原始檔的特點定義的一個介面,該介面描述了對節點內的屬性集的操作介面,除了getAttributeValue、getAttributeCount等一些和XmlPullParser介面相同的介面外。AttributeSet還定義了一些如getIdAttribute、getAttributeResourceValue、getAttributeBooleanValue這些pull解析方式之外的一些帶有android特性的介面,相當於是對節點的屬性集合的操作介面進行了拓展。

這樣看來,XmlBlock#Parser類除了實現了pull解析方式自帶的介面定義外。還實現了AttributeSet介面內定義的一些具有android特性的介面。

但是Parser內並未儲存節點下所有的Attributes(屬性)。這些屬性都是存在android.content.res.TypedArray內,而如何得到TypedArray型別物件,繼續往下看。

回到LayoutInflater#inflate的第四個重構方法,看看是如何使用parser這個xml解析器的。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { // …

    // 因為parser實現了AttributeSet介面,所以這裡是強轉
    final AttributeSet attrs = Xml.asAttributeSet(parser);

    // result是需要return的值
    View result = root;

    try {
        // 通過一個迴圈,尋找根節點
        int type;
        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 (TAG_MERGE.equals(name)) {
            // 如果根節點是merge標籤
            if (root == null || !attachToRoot) {
                // merge標籤要求傳入的ViewGroup不能是空,並且attachToRoot必須為true, 否則報錯
                throw new InflateException("<merge /> can be used only with a valid "
                        + "ViewGroup root and attachToRoot=true");
            }

            // 遞迴生成根節點下的所有子節點
            rInflate(parser, root, inflaterContext, attrs, false);
        } else {
            // 根據節點的資訊(名稱、屬性)生成根節點View物件
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);

            // 根節點的LayoutParams屬性
            ViewGroup.LayoutParams params = null;

            if (root != null) {
                // 如果傳入的ViewGroup不為空

                // 呼叫root的generateLayoutParams方法來生成根節點的LayoutParams屬性物件
                params = root.generateLayoutParams(attrs);
                if (!attachToRoot) {
                    // 不需要講根節點新增到傳入的ViewGroup節點下,則將LayoutParams物件設定到根節點內
                    // 否則的話在後面將會通過addView方式設定params
                    temp.setLayoutParams(params);
                }
            }

            if (DEBUG) {
                System.out.println("-----> start inflating children");
                // 開始解析所有子節點
            }

            // 解析根節點下的子節點
            rInflateChildren(parser, temp, attrs, true);

            if (DEBUG) {
                System.out.println("-----> done inflating children");
                // 結束了所有子節點的解析
            }

            if (root != null && attachToRoot) {
                // 如果傳入的ViewGroup不是空,並且需要新增根節點到其下面
                root.addView(temp, params);
            }

            if (root == null || !attachToRoot) {
                // 如果根節點為空,或者是attachToRoot為false,返回根節點
                result = temp;
            }
        }

    } catch (XmlPullParserException e) {
        // ....
    } catch (Exception e) {
        // ....
    } finally {
        // ....
    }

    // return 結果(根節點或者是傳入的ViewGroup)
    return result;
}

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 這裡有幾個比較關鍵的地方,一一進行分析:

// 根據節點的資訊(名稱、屬性)生成根節點View物件 final View temp = createViewFromTag(root, name, inflaterContext, attrs); 1 2 createViewFromTag方法建立了對應節點的View物件:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { if (name.equals(“view”)) { // 如果節點名字為view,則取節點下面的class屬性作為名字 name = attrs.getAttributeValue(null, “class”); }

// 不使用預設Theme屬性的這部分邏輯跳過不講
if (!ignoreThemeAttr) {
    final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
    final int themeResId = ta.getResourceId(0, 0);
    if (themeResId != 0) {
        context = new ContextThemeWrapper(context, themeResId);
    }
    ta.recycle();
}

// 幾點名稱為blink的時候,建立一個BlinkLayout類物件,繼承自FrameLayout。
if (name.equals(TAG_1995)) {
    // Let's party like it's 1995!
    return new BlinkLayout(context, attrs);
}

try {
    View view;

    // mFactory和mFactory2是兩個工廠類,可以對檢視的建立進行hook,暫時不分析
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    // 和mFactory類似,暫不分析
    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    // 最終會走到這,
    if (view == null) {
        // View的構造方法引數:context
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('.')) {
                // 如果節點名字不帶".",說明是系統提供的View(Button/TextView等),走系統View的建立流程,android.view包下的
                view = onCreateView(parent, name, attrs);
            } else {
                // 否則則說明是自定義View,走自定義View的建立流程
                view = createView(name, null, attrs);
            }
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }

    // 返回解析出來的View
    return view;
} catch (InflateException e) {
    // ...
} catch (ClassNotFoundException e) {
    // ...
} catch (Exception e) {
    // ...
}

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 最終會呼叫LayoutInflater#createView方法來建立指定名字的View(呼叫onCreateView方法最後也會呼叫createView方法):

public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException {

// sConstructorMap儲存了所有解析過的View的構造方法Constructor
Constructor<? extends View> constructor = sConstructorMap.get(name);
// 待解析的View的Class
Class<? extends View> clazz = null;

try {
    if (constructor == null) {
        // 快取中沒有該型別的構造方法,也就是之前沒有解析過該Class型別的View,
        // 通過反射獲取Constructor物件,並快取
        clazz = mContext.getClassLoader().loadClass(
                prefix != null ? (prefix + name) : name).asSubclass(View.class);

        // Filter這個東西是用來攔截節點解析的,
        // onLoadClass返回false的話,將會呼叫failNotAllowed,就是報錯,不允許解析
        if (mFilter != null && clazz != null) {
            boolean allowed = mFilter.onLoadClass(clazz);
            if (!allowed) {
                failNotAllowed(name, prefix, attrs);
            }
        }
        // 反射獲取Constructor物件,並快取
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    } else {
        if (mFilter != null) {
            // 如果有攔截器的話,需要通過快取的攔截資訊判斷是否需要攔截解析,
            // 如果未快取攔截資訊的話,則動態從mFilter#onLoadClass中取出攔截資訊
            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);

                boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                mFilterMap.put(name, allowed);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            } else if (allowedState.equals(Boolean.FALSE)) {
                failNotAllowed(name, prefix, attrs);
            }
        }
    }

    Object[] args = mConstructorArgs;
    // View的構造方法裡第二個引數是AttributeSet,一個用來解析屬性的物件
    args[1] = attrs;

    // View物件的真正建立
    final View view = constructor.newInstance(args);
    if (view instanceof ViewStub) {
        // 如果是ViewStub的話,需要為其設定一個copy的LayoutInflater
        final ViewStub viewStub = (ViewStub) view;
        viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
    }
    // 返回結果
    return view;

} catch (NoSuchMethodException e) {
    // 這個報錯比較重要
    InflateException ie = new InflateException(attrs.getPositionDescription()
            + ": Error inflating class "
            + (prefix != null ? (prefix + name) : name));
    ie.initCause(e);
    throw ie;
} catch (ClassCastException e) {
    // ...
} catch (ClassNotFoundException e) {
    // ...
} catch (Exception e) {
    // ...
} finally {
    // ...
}

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 LayoutInflater是通過反射的方式建立View,並將context以及AttributeSet物件作為引數傳入。

也就是說如果使用者自定義View的時候,沒有重寫帶兩個引數的構造方法的話,將會報錯。程式碼將會走到上面NoSuchMethodException這個catch中。例如下面這個報錯資訊(注意註釋部分):

FATAL EXCEPTION: main Process: com.example.j_liuchaoqun.myapplication, PID: 26075 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.j_liuchaoqun.myapplication/com.example.j_liuchaoqun.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #13: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2793) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864) at android.app.ActivityThread.-wrap12(ActivityThread.java) at android.app.ActivityThreadH.handleMessage(ActivityThread.java:1567)atandroid.os.Handler.dispatchMessage(Handler.java:102)atandroid.os.Looper.loop(Looper.java:156)atandroid.app.ActivityThread.main(ActivityThread.java:6524)atjava.lang.reflect.Method.invoke(NativeMethod)atcom.android.internal.os.ZygoteInitH.handleMessage(ActivityThread.java:1567) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:156) at android.app.ActivityThread.main(ActivityThread.java:6524) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInitMethodAndArgsCaller.run(ZygoteInit.java:941) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831) Caused by: android.view.InflateException: Binary XML file line #13: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView Caused by: android.view.InflateException: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView

// 大家主要看下面這行資訊,在createView(LayoutInflater.java:625)方法中反射時,提示缺少一個SlideTextView(Context context, AttributeSet set);的構造方法。

Caused by: java.lang.NoSuchMethodException: [class android.content.Context, interface android.util.AttributeSet] at java.lang.Class.getConstructor0(Class.java:2204) at java.lang.Class.getConstructor(Class.java:1683) at android.view.LayoutInflater.createView(LayoutInflater.java:625) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:798) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:738) at android.view.LayoutInflater.rInflate(LayoutInflater.java:869) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:832) at android.view.LayoutInflater.inflate(LayoutInflater.java:518) at android.view.LayoutInflater.inflate(LayoutInflater.java:426) at android.view.LayoutInflater.inflate(LayoutInflater.java:377) at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:255) at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:109) at com.example.j_liuchaoqun.myapplication.MainActivity.onCreate(MainActivity.java:11) at android.app.Activity.performCreate(Activity.java:6910) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2746) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864) at android.app.ActivityThread.-wrap12(ActivityThread.java) at android.app.ActivityThreadH.handleMessage(ActivityThread.java:1567)atandroid.os.Handler.dispatchMessage(Handler.java:102)atandroid.os.Looper.loop(Looper.java:156)atandroid.app.ActivityThread.main(ActivityThread.java:6524)atjava.lang.reflect.Method.invoke(NativeMethod)atcom.android.internal.os.ZygoteInitH.handleMessage(ActivityThread.java:1567) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:156) at android.app.ActivityThread.main(ActivityThread.java:6524) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInitMethodAndArgsCaller.run(ZygoteInit.java:941) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 在API21中,將會呼叫到View的一個四個引數的構造方法,低版本API中可能只有三個構造方法,但不管如何,最後都會呼叫到引數最多的那個構造方法,並在該方法中對View進行初始化,而初始化的資訊,都將通過AttributeSet生成的TypedArray物件來獲取。

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { this(context);

// 解析styleable.View的所有屬性
final TypedArray a = context.obtainStyledAttributes(
        attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

// ...

// 遍歷解析出來的所有屬性,並設定為當前View物件
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
    int attr = a.getIndex(i);
    switch (attr) {
        case com.android.internal.R.styleable.View_background:
            // 背景
            background = a.getDrawable(attr);
            break;
        }
        // ...其他case
        default:
            break;
    }
}

// ...

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 這裡對其構造方法進行了簡化,可以看到,AttributeSet是在這裡使用的,通過context.obtainStyledAttributes方法將attrs.xml下定義的View這個styable屬性集解析出來,android原始碼中的attrs.xml檔案中定義了View的所有屬性:

<!-- Supply a tag for this view containing a String, to be retrieved
     later with {@link android.view.View#getTag View.getTag()} or
     searched for with {@link android.view.View#findViewWithTag
     View.findViewWithTag()}.  It is generally preferable to use
     IDs (through the android:id attribute) instead of tags because
     they are faster and allow for compile-time type checking. -->
<attr name="tag" format="string" />

<!-- The initial horizontal scroll offset, in pixels.-->
<attr name="scrollX" format="dimension" />

<!-- The initial vertical scroll offset, in pixels. -->
<attr name="scrollY" format="dimension" />

<!-- A drawable to use as the background.  This can be either a reference
     to a full drawable resource (such as a PNG image, 9-patch,
     XML state list description, etc), or a solid color such as "#ff000000"
    (black). -->
<attr name="background" format="reference|color" />

<!-- Sets the padding, in pixels, of all four edges.  Padding is defined as
     space between the edges of the view and the view's content. A views size
     will include it's padding.  If a {@link android.R.attr#background}
     is provided, the padding will initially be set to that (0 if the
     drawable does not have padding).  Explicitly setting a padding value
     will override the corresponding padding found in the background. -->
<attr name="padding" format="dimension" />
<!-- Sets the padding, in pixels, of the left edge; see {@link android.R.attr#padding}. -->
<attr name="paddingLeft" format="dimension" />
<!-- Sets the padding, in pixels, of the top edge; see {@link android.R.attr#padding}. -->
<attr name="paddingTop" format="dimension" />
<!-- Sets the padding, in pixels, of the right edge; see {@link android.R.attr#padding}. -->
<attr name="paddingRight" format="dimension" />
<!-- Sets the padding, in pixels, of the bottom edge; see {@link android.R.attr#padding}. -->
<attr name="paddingBottom" format="dimension" />
<!-- Sets the padding, in pixels, of the start edge; see {@link android.R.attr#padding}. -->
<attr name="paddingStart" format="dimension" />
<!-- Sets the padding, in pixels, of the end edge; see {@link android.R.attr#padding}. -->
<attr name="paddingEnd" format="dimension" />

<!-- 屬性太多,不一一列舉 -->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 當然,如果你是View的子類,也有對應的屬性,比如ListView: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 對應在ListView的構造方法裡有:

public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes);

// ...

final TypedArray a = context.obtainStyledAttributes(
        attrs, R.styleable.ListView, defStyleAttr, defStyleRes);

// 從節點中獲取Divider屬性,如果有定義的話,設定到ListView中
final Drawable d = a.getDrawable(R.styleable.ListView_divider);
if (d != null) {
    // Use an implicit divider height which may be explicitly
    // overridden by android:dividerHeight further down.
    setDivider(d);
}

// 其他ListView提供的屬性...

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 至此,xml中根節點的解析過程告一段落。

那麼LayoutInflater是如何解析xml下的其他子節點的? 回過頭來看LayoutInflater#inflate第四個重構方法裡有一段程式碼:

// 解析根節點下的子節點 rInflateChildren(parser, temp, attrs, true); 1 2 該方法將會遍歷View的所有子節點,並呼叫createViewFromTag對每一個節點進行解析,並把解析出來的View新增到父節點中。具體內如如何實現,大家可以看看原始碼。與xml的根節點解析類似。

inflate方法的attachToRoot(Boolean)引數 attachToRoot是inflate接收的一個引數,它有兩重作用:

表示是否需要將解析出來的xml根節點add到傳入的root佈局中(如果root不為空的話)。 如果attachToRoot為true,則inflate方法將返回root物件,否則,將返回解析出來的xml根節點View物件。 inflate方法的root(ViewGroup)引數 如果root不為空,將會呼叫root的generateLayoutParams方法為xml跟佈局生成LayoutParams物件。generateLayoutParams是ViewGroup中定義的方法。它的子類可以對其進行重寫,以返回對應型別的LayoutParams

FrameLayout#generateLayoutParams(android.util.AttributeSet):

@Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new FrameLayout.LayoutParams(getContext(), attrs); } 1 2 3 4 RelativeLayout#generateLayoutParams(android.util.AttributeSet):

@Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new RelativeLayout.LayoutParams(getContext(), attrs); } 1 2 3 4 可以發現,如果傳入的root是FrameLayout型別的話,將會生成FrameLayout.LayoutParams,如果傳入的root是RelativeLayout型別的話,將會生成RelativeLayout.LayoutParams。

根據這樣的規律,分析下面兩種情況:

  1. xml根節點定義了屬性android:layout_centerHorizontal=”true”,而inflate方法傳入的root物件為FrameLayout型別,此時android:layout_centerHorizontal將會失效,因為FrameLayout.LayoutParam物件並不支援layout_centerHorizontal屬性。
  2. xml根節點定義了屬性android:layout_gravity=”center”,而inflate方法傳入的的root物件為RelativeLayout型別,此時android:layout_gravity也會失效,因為RelativeLayout.LayoutParams並不支援layout_gravity屬性。
  3. 同理還需要考慮LinearLayout.LayoutParams所支援的屬性與xml根節點定義的屬性是否有衝突。

如果傳入的root物件為空,xml根節點的所有的以“layout_”開頭的屬性都將失效,因為沒有root物件來為根節點生成對應的LayoutParams物件。

針對該特性,如果傳入的root為空,將出現類似如根節點定義的寬高失效,如我定義的根節點寬度為50dp,高度也為50dp,最後顯示出來的效果卻是一個wrap_content的效果。為什麼會出現上述原因,是因為如果根節點沒有LayoutParams物件,那麼在它被add到某一個ViewGroup上的時候,將會自動生成一個寬高為wrap_content的LayoutParams物件:

ViewGroup#addView(android.view.View, int):

public void addView(View child, int index) { if (child == null) { throw new IllegalArgumentException(“Cannot add a null child view to a ViewGroup”); } LayoutParams params = child.getLayoutParams(); if (params == null) { // 如果LayoutParams為空的話,生成預設的 params = generateDefaultLayoutParams(); if (params == null) { throw new IllegalArgumentException(“generateDefaultLayoutParams() cannot return null”); } } addView(child, index, params); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ViewGroup#generateDefaultLayoutParams:

protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } 1 2 3 總結 LayoutInflater是android用來解析xml佈局檔案的一個類 LayoutInflater內部使用Pull解析的方式,並對其進行了一定的擴充套件。 LayoutInflater在生成View節點的時候,是通過反射的方式建立View物件, 反射呼叫的構造方法是帶兩個引數的那個,所以在定義View的時候必須重寫帶兩個引數的構造方法。 LayoutInflater在建立View物件的時候,會將xml節點的解析器AttributeSet傳入到View的構造方法中。AttributeSet定義了用來解析xml節點屬性的API。View通過AttributeSet生成TypedArray,並從中讀取Vie