Android LayoutInflater 原始碼解析
大家對LayoutInflater一定不陌生,它主要用於載入佈局,在Fragment的onCreateView方法、ListView Adapter的getView方法等許多地方都可以見到它的身影。今天主要聊聊LayoutInflater的用法以及載入佈局的工作原理。
什麼是LayoutInflater
LayoutInflater是一個用於將xml佈局檔案載入為View或者ViewGroup物件的工具,我們可以稱之為佈局載入器。
用法
獲取LayoutInflater
首先要注意LayoutInflater本身是一個抽象類,我們不可以直接通過new
的方式去獲得它的例項,通常有下面三種方式:
第一種:
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
第二種:
LayoutInflater inflater = LayoutInflater.from(context);
第三種:
在Activity內部呼叫getLayoutInflater()方法
看看後面兩種方法的實現:
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; }
在Activity內部呼叫getLayoutInflater方法其實呼叫的是PhoneWindow的mLayoutInflater:
public PhoneWindow(Context context) { super(context); mLayoutInflater = LayoutInflater.from(context); }
所以,這幾個方法實際上殊途同歸,都是通過呼叫Context的getSystemService方法去獲取。獲取到的是PhoneLayoutInflater
這個實現類,具體的獲取過程就不在這裡展開分析了。
public class Policy implements IPolicy { ... public LayoutInflater makeNewLayoutInflater(Context context) { return new PhoneLayoutInflater(context); } }
載入佈局
我們用一個簡單的例子,介紹下LayoutInflater的用法:
這個例子的目標是在螢幕上展示一個按鈕,點選按鈕時,會通過LayoutInflater把一個橙色背景的TextView以
match_parent
的形式載入到一塊寬高為300dp的RelativeLayout中。
首先建立兩個佈局檔案:
demo_layout.xml
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:gravity="center" android:background="#ff750c" android:text="Hello , world !" />
activity_main.xml
<?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"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:text="點選載入" /> <RelativeLayout android:id="@+id/root" android:layout_width="300dp" android:layout_height="300dp" android:layout_centerInParent="true"/> </RelativeLayout>
MainActivity.java
public class MainActivity extends Activity { RelativeLayout rootView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); rootView = (RelativeLayout) findViewById(R.id.root); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { inflateView(); } }); } private void inflateView() { View insideView = LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout, null); rootView.addView(insideView); } }
編譯執行,點選點選載入
按鈕,結果如下:
可以看到,我們成功把demo_layout.xml對應佈局中的TextView載入進來了。
但遺憾的是,載入進來的TextView寬高並不是我們期望的300×300大小╮(╯▽╰)╭。
那麼問題來了:
- 為什麼我們在佈局檔案中給TextView設定的寬高屬性失效了呢?
- LayoutInflater又是如何把xml解析載入成為View的呢?
而且,inflate有多個不同的過載方法:
inflate(int resource, ViewGroup root)
inflate(int resource, ViewGroup root, boolean attachToRoot)
inflate(XmlPullParser parser, ViewGroup root)
inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
上面的例子只是使用了第一個方法,並且root的傳參還是null。
這些方法又有什麼不同的地方呢?
我們從原始碼入手,去尋找這兩個問題的答案。
原始碼解析
上文有提到,我們獲取的LayoutInflater例項其實是PhoneLayoutInflater,但PhoneLayoutInflater並沒有重寫inflate的幾個方法,所以我們的分析還是在LayoutInflater這個類展開。
首先比對下這幾個過載方法:
public View inflate(int resource, ViewGroup root) { // root不為空時,attachToRoot預設為true return inflate(resource, root, root != null); } public View inflate(int resource, ViewGroup root, boolean attachToRoot) { XmlResourceParser parser = getContext().getResources().getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } public View inflate(XmlPullParser parser, ViewGroup root) { // root不為空時,attachToRoot預設為true return inflate(parser, root, root != null); } public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { ... }
原來,前三個方法最終呼叫的都是:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { ... }
而且,root不為空時,attachToRoot預設為true。佈局id會被通過呼叫getLayout
方法生成一個XmlResourceParser
物件。
Android中佈局檔案都是使用xml編寫的,所以解析過程自然涉及xml的解析。常用的xml解析方式有DOM,SAX和PULL三種方式。DOM不適合xml文件較大,記憶體較小的場景,所以不適用於手機這樣記憶體有限的移動裝置上。SAX和PULL類似,都具有解析速度快,佔用記憶體少的優點,而相對之下,PULL的操作方式更為簡單易用,所以,Android系統內部在解析各種xml時都用的是PULL解析器。
這裡解析佈局xml檔案時使用的就是Android系統提供的PULL方式。
我們繼續分析inflate方法。
inflate方法
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); // 首先注意result初值為root View result = root; try { // 嘗試找到佈局檔案的根節點 int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } ... // 獲取當前節點名稱,如merge,RelativeLayout等 final String name = parser.getName(); ... // 處理merge節點 if (TAG_MERGE.equals(name)) { // merge必須依附在一個根View上 if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, attrs, false); } else { View temp; // 根據當前資訊生成一個View temp = createViewFromTag(root, name, attrs); ... ViewGroup.LayoutParams params = null; if (root != null) { // 如果指定了root引數的話,根據節點的佈局引數生成合適的LayoutParams params = root.generateLayoutParams(attrs); // 若指定了attachToRoot為false,會將生成的佈局引數應用於上一步生成的View if (!attachToRoot) { temp.setLayoutParams(params); } } // 由上至下,遞迴載入xml內View,並新增到temp裡 rInflate(parser, temp, attrs, true); // 如果root不為空且指定了attachToRoot為true時,會將temp作為子View新增到root中 if (root != null && attachToRoot) { root.addView(temp, params); } // 如果指定的root為空,或者attachToRoot為false的時候,返回的是載入出來的View, // 否則返回root if (root == null || !attachToRoot) { result = temp; } } } ... // 異常處理 return result; } }
首先定義佈局根View這一個概念,注意與root並不是同一個東西:
- root是我們傳進來的第二個引數
- 佈局根View則是傳遞進來的佈局檔案的根節點所對應的View
這個方法主要有下面幾個步驟:
- 首先查詢根節點,如果整個xml檔案解析完畢也沒看到根節點,會丟擲異常;
- 如果查詢到的根節點名稱是merge標籤,會呼叫rInflate方法繼續解析佈局,最終返回root;
- 如果是其他標籤(View、TextView等),會呼叫createViewFromTag生成佈局根View,並呼叫rInflate遞迴解析餘下的子View,新增至佈局根View中,最後視root和attachToRoot引數的情況最終返回view或者root。
從這裡我們可以理清root和attachToRoot引數的關係了:
root != null, attachToRoot == true:
傳進來的佈局會被載入成為一個View並作為子View新增到root中,最終返回root;
而且這個佈局根節點的android:layout_引數會被解析用來設定View的大小。root == null, attachToRoot無用:
當root為空時,attachToRoot是什麼都沒有意義,此時傳進來的佈局會被載入成為一個View並直接返回;
佈局根View的android:layout_xxx屬性會被忽略。root != null, attachToRoot == false:
傳進來的佈局會被載入成為一個View並直接返回。
佈局根View的android:layout_xxx屬性會被解析成LayoutParams並保留。(root只用來參與生成佈局根View的LayoutParams)
現在可以解答文章開始留下的疑問了:
為何在佈局檔案中給TextView設定的android:layout屬性失效了?
回到例子中的程式碼,我們載入佈局的程式碼是:
View insideView = LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout, null); rootView.addView(insideView);
即root傳參為空,與上面第2種情況對應,所以此時佈局根View的android:layout_xx屬性都被忽略了。也就是相當於並沒有給TextView設定寬高,所以只能按預設的TextView大小顯示了。
稍微改變下程式碼:
LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout, rootView);
注意這段程式碼等同於:
LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout, rootView, true);
inflate方法在root不為空時,預設會將attachToRoot置為true。
這時等同於我們上面的情況1,由於此時infalte會將載入出來的View自動新增到root中,我們要把rootView.addView(insideView)
一句移除,否則會遇到這樣的報錯:
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first. at android.view.ViewGroup.addViewInner(ViewGroup.java:4454) at android.view.ViewGroup.addView(ViewGroup.java:4295) at android.view.ViewGroup.addView(ViewGroup.java:4235) at android.view.ViewGroup.addView(ViewGroup.java:4208)
再來執行看看:
終於達到我們想要的效果了!這也驗證了上面的第一個結論。
順便再用這個例子擴充一下,驗證我們的情況3,即root != null, attachToRoot == false
時的情況:
View insideView = LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout, rootView, false); rootView.addView(insideView);
結果是一樣的,圖就不貼了,即root != null, attachToRoot == false
時,root只是用來參與佈局根View的大小、位置設定的。
好了,關於這兩個引數的疑問的解答就告一段落了,我們接著回到程式碼,尋找另一個問題的答案。
繼續跟進rInflate和createViewFromTag方法。
rInflate方法
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { ... while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { ... final String name = parser.getName(); // 解析“requestFocus”標籤,讓父View呼叫requestFocus()獲取焦點 if (TAG_REQUEST_FOCUS.equals(name)) { ... } else if (TAG_INCLUDE.equals(name)) { ... } else if (TAG_MERGE.equals(name)) { ... } else if (TAG_1995.equals(name)) { ... } else { // 呼叫createViewFromTag生成一個View final View view = createViewFromTag(parent, name, attrs); // 逐層遞迴呼叫rInflate,解析view巢狀的子View rInflate(parser, view, attrs, true); // 將解析生成子View新增到上一層View中 viewGroup.addView(view, params); } } // 內層子View被解析出來後,將呼叫其父View的“onFinishInflate()”回撥 if (finishInflate) parent.onFinishInflate(); }
首先是幾個特殊標籤的處理,如requestFocus
、include
等,為了把握住主要脈絡,我們不做展開,直接看最後一個else的內容。
原來,rInflate主要是呼叫了createViewFromTag生成當前解析到的View節點,並遞迴呼叫rInflate逐層生成子View,新增到各自的上層View節點中。
當某個節點下面的所有子節點View解析生成完成後,才會調起onFinishInflate回撥。
所以createViewFromTag才是真正生成View的地方啊。
createViewFromTag方法
View createViewFromTag(View parent, String name, AttributeSet attrs) { ... try { View view; if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs); else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs); else view = null; if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, mContext, attrs); } // 三個Factory都不存在呼叫LayoutInflater自己的onCreateView或者createView // // 如果View標籤中沒有".",則代表是系統的widget,則呼叫onCreateView, // 這個方法會通過"createView"方法建立View // 不過字首欄位會自動補"android.view."字首。 if (view == null) { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } return view; } catch (InflateException e) { ... } ... } public interface Factory { public View onCreateView(String name, Context context, AttributeSet attrs); }
首先會依次呼叫mFactory2、mFactory和mPrivateFactory三者之一的onCreateView方法去建立一個View。
如果這幾個Factory都為null,會呼叫LayoutInflater自己的onCreateView或者createView來例項化View。
自定義Factory一個十分有用的使用場景就是實現應用換膚,有興趣的讀者可以參考我開源的Android-Skin-Loader中的具體細節。
通常情況下,自定義工廠mFactory2、mFactory和私有工廠mPrivateFactory是空的,當Activity繼承自AppCompatActivity時,才會存在自定義Factory。
所以,生成View的重任就落在了onCreateView和createView身上。
onCreateView呼叫的其實是createView,即View的節點名稱沒有.
時,將自動補上android.view.
字首(即完整類名):
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs); }
繼續關注的createView實現。
createView方法
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; try { if (constructor == null) { // 快取中不存在某View的構造方法,先new出來放快取中 clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); ... constructor = clazz.getConstructor(mConstructorSignature); sConstructorMap.put(name, constructor); } else { ... } Object[] args = mConstructorArgs; args[1] = attrs; return constructor.newInstance(args); } catch (NoSuchMethodException e) { ... } }
這就是最後一步了,十分容易理解:
通過傳進來的全類名,呼叫newInstance
來建立一個這個類的例項並返回,返回的這個例項就是我們需要的View了。
結合上面的遞迴解析過程,每個層級的節點都會被生成一個個的View,並根據View的層級關係add到對應的直接父View(上層節點)中,最終返回一個包含了所有解析好的子View的佈局根View。
至此,通過xml來載入View的整個原理就分析完成了。
總結
最後,我們再次回顧下上面的分析結果:
inflate方法的引數關係
root != null, attachToRoot == true
傳進來的佈局會被載入成為一個View並作為子View新增到root中,最終返回root;
而且這個佈局根節點的android:layout_xxx引數會被解析用來設定View的大小;root == null, attachToRoot無意義
當root為空時,attachToRoot無論是什麼都沒有意義。此時傳進來的佈局會被載入成為一個View並直接返回;
佈局根View的android:layout_xxx
屬性會被忽略,即android:layout_xx屬性只有依附在某個ViewGroup中才能生效;root != null, attachToRoot == false
傳進來的佈局會被載入成為一個View並直接返回。
佈局根View的android:layout_xxx
屬性會被解析成LayoutParams並設定在View上,此時root只用於設定佈局根View的大小和位置。
載入xml佈局的原理
其實就是從根節點開始,遞迴解析xml的每個節點,每一步遞迴的過程是:通過節點名稱(全類名),使用ClassLoader建立對應類的例項,也就是View,然後,將這個View新增到它的上層節點(父View)。並同時會解析對應xml節點的屬性作為View的屬性。每個層級的節點都會被生成一個個的View,並根據View的層級關係add到對應的直接父View(上層節點)中,最終返回一個包含了所有解析好的子View的佈局根View。
相關文章
- Android LayoutInflater Factory 原始碼解析Android原始碼
- 【Android原始碼】LayoutInflater 分析Android原始碼
- Android原始碼分析(LayoutInflater.from(this).inflate(resId,null);原始碼解析)Android原始碼Null
- LayoutInflater建立View原始碼閱讀View原始碼
- Android顯示框架:Android佈局解析者LayoutInflaterAndroid框架
- Android Handler 原始碼解析Android原始碼
- Android Retrofit原始碼解析Android原始碼
- Android——LruCache原始碼解析Android原始碼
- android LruCache原始碼解析Android原始碼
- Android EventBus原始碼解析Android原始碼
- android原始碼解析--switchAndroid原始碼
- Android原始碼解析--LooperAndroid原始碼OOP
- android LayoutInflater、setContentView、findviewbyid 區分解析AndroidView
- View 繪製體系知識梳理(1) LayoutInflater#inflate 原始碼解析View原始碼
- Android 開源專案原始碼解析 -->PhotoView 原始碼解析(七)Android原始碼View
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- Android原始碼解析-LiveDataAndroid原始碼LiveData
- [Android] Retrofit原始碼:流程解析Android原始碼
- Android 8.1 Handler 原始碼解析Android原始碼
- WebRTC-Android原始碼解析WebAndroid原始碼
- android原始碼解析--DialogAndroid原始碼
- android原始碼解析--MessageQueueAndroid原始碼
- android原始碼解析--MessageAndroid原始碼
- Android fragment原始碼全解析AndroidFragment原始碼
- android原始碼解析--ListView(上)Android原始碼View
- Android 中LayoutInflater(佈局載入器)原始碼篇之rInflate方法Android原始碼
- Android 開源專案原始碼解析 -->Volley 原始碼解析(十五)Android原始碼
- Android 開源專案原始碼解析 -->Dagger 原始碼解析(十三)Android原始碼
- Android 開源專案原始碼解析 -->CircularFloatingActionMenu 原始碼解析(八)Android原始碼
- Android setContentView原始碼解析AndroidView原始碼
- Android Volley框架原始碼解析Android框架原始碼
- LayoutInflater原始碼分析與應用 | 掘金技術徵文原始碼
- Android系統原始碼目錄解析Android原始碼
- Android 網路框架 Retrofit 原始碼解析Android框架原始碼
- Android AccessibilityService機制原始碼解析Android原始碼
- weex原始碼解析(四)- android引入sdk原始碼Android
- Android原始碼解析(二)動畫篇-- ObjectAnimatorAndroid原始碼動畫Object
- Android開源庫——EventBus原始碼解析Android原始碼