Android-LayoutInflater佈局文件解析過程分析

備註:

本篇文章所引用的源碼版本:android-sdk-21

轉載請註明出處:http://blog.csdn.net/a740169405/article/details/54580347

簡述:

簡單的說,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);
    }
}

從代碼中可以發現,除了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());
}

這裏提一下,上面代碼是android-sdk-21版本的源碼,創建了一個BridgeInflater對象,如果是android-sdk-19及以下的源碼,PolicyManager#makeNewLayoutInflater方法應該是:

public static LayoutInflater makeNewLayoutInflater(Context context) {
    return sPolicy.makeNewLayoutInflater(context);
}

接着調用了com.android.internal.policy.impl.Policy#makeNewLayoutInflater(Context context)方法:

public LayoutInflater makeNewLayoutInflater(Context context) {
    return new PhoneLayoutInflater(context);
}

也就是說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);
}

該方法從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;
}

最終該方式還是調用了方式一中說到的Context#getSystemService(String name);方法,並將LayoutInflater服務名稱傳遞進去。

方式三:

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

@NonNull
public LayoutInflater getLayoutInflater() {
    return getWindow().getLayoutInflater();
}

Activity裏的getWindow返回的是一個PhoneWindow對象,接着看PhoneWindow#getLayoutInflater();

@Override
public LayoutInflater getLayoutInflater() {
    return mLayoutInflater;
}

返回了一個LayoutInflater對象,其初始化是在PhoneWindow的構造方法裏:

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}

其最終調用了方式二中的LayoutInflater#from(Context context);方法。


佈局解析過程

接着,分析LayoutInflater是如何將一個xml佈局文件解析成一個View對象的。涉及到以下內容:

  1. LayoutInflater#inflate(…);的四個重構方法
  2. 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. 第二個重構方法最終調用了第四個重構方法

第一個:

public View inflate(int resource, ViewGroup root) {
    // 調用第三個重構方法
    return inflate(resource, root, root != null);
}

第二個:

public View inflate(XmlPullParser parser, ViewGroup root) {
    // 調用第四個重構方法
    return inflate(parser, root, root != null);
}

第三個:

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();
    }
}

第四個:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    // 省略內容,後面分析
}

真正開始佈局的解析流程的是第四個重構方法,也就是說我們只要分析第四個重構方法的流程就能知道xml佈局文件是如何被解析的。

LayoutInflater#inflate(…);是如何解析視圖的

視圖的解析過程可以總結成:

  1. 使用XmlPullParser遍歷xml文件內的所有節點
  2. 在遍歷到某一節點時,根據節點名字生成對應的View對象
  3. 在生成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);

res是Resource類對象,resource是資源文件id,看看Resource#getLayout(int id);方法的實現:

public XmlResourceParser getLayout(int id) throws NotFoundException {
    return loadXmlResourceParser(id, "layout");
}

Resource#loadXmlResourceParser(int id, String type);方法最終將會返回一個XmlBlock#Parser類型的對象:

final class XmlBlock {
    // ...
    final class Parser implements XmlResourceParser {
        // ...
    }
    // ...
}

XmlResourceParser繼承自XmlPullParser、AttributeSet以及AutoCloseable(一個定義了不使用時需要關閉的接口):

public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {

    public void close();
}

也就是說最終返回了一個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;
    }
}

這裏有幾個比較關鍵的地方,一一進行分析:

// 根據節點的信息(名稱、屬性)生成根節點View對象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

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) {
        // ...
    }
}

最終會調用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 {
        // ...
    }
}

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.ActivityThread$H.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.ZygoteInit$MethodAndArgsCaller.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: <init> [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.ActivityThread$H.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.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:941)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831)

在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;
        }
    }

    // ...
}

這裏對其構造方法進行了簡化,可以看到,AttributeSet是在這裏使用的,通過context.obtainStyledAttributes方法將attrs.xml下定義的View這個styable屬性集解析出來,android源碼中的attrs.xml文件中定義了View的所有屬性:

<!-- Attributes that can be used with {@link android.view.View} or
     any of its subclasses.  Also see {@link #ViewGroup_Layout} for
     attributes that are processed by the view's parent. -->
<declare-styleable name="View">
    <!-- Supply an identifier name for this view, to later retrieve it
         with {@link android.view.View#findViewById View.findViewById()} or
         {@link android.app.Activity#findViewById Activity.findViewById()}.
         This must be a
         resource reference; typically you set this using the
         <code>@+</code> syntax to create a new ID resources.
         For example: <code>android:id="@+id/my_id"</code> which
         allows you to later retrieve the view
         with <code>findViewById(R.id.my_id)</code>. -->
    <attr name="id" format="reference" />

    <!-- 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" />

    <!-- 屬性太多,不一一列舉 -->
</declare-styleable>

當然,如果你是View的子類,也有對應的屬性,比如ListView:

<declare-styleable name="ListView">
    <!-- Reference to an array resource that will populate the ListView.  For static content,
         this is simpler than populating the ListView programmatically. -->
    <attr name="entries" />
    <!-- Drawable or color to draw between list items. -->
    <attr name="divider" format="reference|color" />
    <!-- Height of the divider. Will use the intrinsic height of the divider if this
         is not specified. -->
    <attr name="dividerHeight" format="dimension" />
    <!-- When set to false, the ListView will not draw the divider after each header view.
         The default value is true. -->
    <attr name="headerDividersEnabled" format="boolean" />
    <!-- When set to false, the ListView will not draw the divider before each footer view.
         The default value is true. -->
    <attr name="footerDividersEnabled" format="boolean" />
    <!-- Drawable to draw above list content. -->
    <attr name="overScrollHeader" format="reference|color" />
    <!-- Drawable to draw below list content. -->
    <attr name="overScrollFooter" format="reference|color" />
</declare-styleable>

對應在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提供的屬性...
}

至此,xml中根節點的解析過程告一段落。

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

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

該方法將會遍歷View的所有子節點,並調用createViewFromTag對每一個節點進行解析,並把解析出來的View添加到父節點中。具體內如如何實現,大家可以看看源碼。與xml的根節點解析類似。

inflate方法的attachToRoot(Boolean)參數

attachToRoot是inflate接收的一個參數,它有兩重作用:

  1. 表示是否需要將解析出來的xml根節點add到傳入的root佈局中(如果root不爲空的話)。
  2. 如果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);        
}

RelativeLayout#generateLayoutParams(android.util.AttributeSet):

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new RelativeLayout.LayoutParams(getContext(), attrs);
}

可以發現,如果傳入的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);
}

ViewGroup#generateDefaultLayoutParams:

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

總結

  1. LayoutInflater是android用來解析xml佈局文件的一個類
  2. LayoutInflater內部使用Pull解析的方式,並對其進行了一定的擴展。
  3. LayoutInflater在生成View節點的時候,是通過反射的方式創建View對象,
    反射調用的構造方法是帶兩個參數的那個,所以在定義View的時候必須重寫帶兩個參數的構造方法。
  4. LayoutInflater在創建View對象的時候,會將xml節點的解析器AttributeSet傳入到View的構造方法中。AttributeSet定義了用來解析xml節點屬性的API。View通過AttributeSet生成TypedArray,並從中讀取View節點中定義的屬性。
  5. 最後LayoutInflater將會通過遞歸的方式創建xml根節點下的所有孩子節點。
  6. LayoutInflater#inflate方法接收一個root對象以及一個Boolean類型的attachToRoot變量。這兩個參數的值,直接影響了inflate方法的返回值,以及生成的xml根節點的LayoutParams和屬性。
發佈了66 篇原創文章 · 獲贊 113 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章