Android XML 实例化的过程
安卓提供了XML布局方式,但是我們必須明白,XML布局最終也是通過xml的pull解析方式,得到布局名稱和控件名稱,以及相關的屬性,然后利用反射機制創建的java對象的,所以效率上來說,java代碼要比XML布局高不少,也更安全。但是寫java代碼又比XML寫起來更麻煩,更不直觀。下面我們來分析一下XML轉換成java對象的過程。
我們通常通過以下方式把一個XML布局轉換成java對象:
通過View的靜態方法inflate:
獲取LayoutInflater對象,調用inflate:
LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.resourceid, null);那么兩種方法之間有什么區別呢?答案是沒有區別。
以下是view中的inflate方法:
我們發現該方法實際也是獲取了LayoutInflater對象,調用了inflate方法。只不過是通過LayoutInflater的靜態方法from。
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; }以上是LayoutInflater的from方法,最終我們發任然現是通過Context的靜態方式獲取,為什么不直接通過new的方式獲取LayoutInflater呢?
因為LayoutInflater的構造方法為protected的,我們是無法直接使用的。
為什么google要把這個類交由系統進行創建,而不由我們程序員來創建呢,我猜想該類用于填充一個XML的布局,需要用到一些系統的環境,那么我們來看看LayoutInflater是如何被創建的吧。
我們知道,該類是由Context的靜態方法獲取的:
public abstract Object getSystemService(String name);我們發現,Context并沒有給我們一個交代,因為該方法竟然是一個抽象的,只剩下查看Context的實現類了。
Context有很多子類,直接子類有ContextWapper(實際是Context的包裝類,該類直接把所有方法轉手又交給了Context),MockContext(該類是Context的模擬類,做法更簡單,直接拋出異常)。而其他子類例如Application、Activity、Service等都沒有返回LayoutInflater。
實際上,對于Context,還有系統給出了一個實現類ContextImpl,該類在\frameworks\base\core\java\android\app\ContextImpl.java,SDK的源碼無法查看,需要Frameworks層的源碼。
public Object getSystemService(String name){if (LAYOUT_INFLATER_SERVICE.equals(name)) { synchronized (mSync) { LayoutInflater inflater = mLayoutInflater; //是否已經賦值,如果是,直接返回引用 if (inflater != null) { return inflater; } //返回一個LayoutInflater對象,getOuterContext()指的是我們的Activity、Service或者Application引用 mLayoutInflater = inflater = PolicyManager.makeNewLayoutInflater(getOuterContext()); return inflater; } } ... //下面是對其他系統服務的獲取,就不再詳述 }我們發現Context又把這個問題甩給了PolicyManager,繼續去看PolicyManager吧。該類在\frameworks\base\core\java\com\android\internal\policy\PolicyManager.java
public final class PolicyManager { private static final String POLICY_IMPL_CLASS_NAME = "com.android.internal.policy.impl.Policy"; private static final IPolicy sPolicy; // 這可不是Binder機制額,這只是是一個接口,別想多啦 static { //我們可以看到該類直接在靜態代碼塊中使用了反射創建了實例對象。try { Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME); sPolicy = (IPolicy)policyClass.newInstance(); } ... } ... public static LayoutInflater makeNewLayoutInflater(Context context) { return sPolicy.makeNewLayoutInflater(context); //繼續去實現類中去查找 } }好吧,IPolicy又一次的把問題拋出了,我們繼續去追查它的實現類Policy類。路徑:/frameworks/base/policy/src/com/android/internal/policy/impl/Policy.java
public class Policy implements IPolicy{ ... public PhoneLayoutInflater makeNewLayoutInflater(Context context) { //實際上返回的是PhoneLayoutInflater類。 return new PhoneLayoutInflater(context); } } 繼續追查PhoneLayoutInflater //PhoneLayoutInflater繼承至LayoutInflater類 public class PhoneLayoutInflater extends LayoutInflater { ... public PhoneLayoutInflater(Context context) { super(context); } ... }終于,它調用了父類的的構造方法,返回了實例對象,所以我們拿到的實際上是一個PhoneLayoutInflater。為什么繞這么一個大圈,最終又回到了原點呢,我猜想就是為了在使用之前,確保是由PolicyManager來創建,該類是一個策略管理者,在Frameworks層,它創建了很多其他對象,類似于一個工廠,而該類是hide隱藏的,外部無法訪問,這樣由它來產生,各大手機廠商都可以根據修改它來實現自身的策略,并給出各自特有的PhoneLayoutInflater,而開發者只用知道外部的統一API即可調用。終于明白了LayoutInflater的來源,接下來我們可以看看inflate方法是如何把一個XML文件轉變成java的view對象。
那么我們再來看看LayoutInflater的inflate方法,看看它是如何把一個XML布局轉變成java對象的。
public View inflate(int resource, ViewGroup root) { return inflate(resource, root, root != null); //調用了重構方法 } public View inflate(int resource, ViewGroup root, boolean attachToRoot) {//可以看到,通過我們傳遞的資源ID,返回了一個Xml的parser對象。XmlResourceParser parser = getContext().getResources().getLayout(resource);try {//繼續調用它的重構方法return inflate(parser, root, attachToRoot);} finally {parser.close();} }我們發現XmlResourceParser是一個繼承自XmlPullParser和AttributeSet的接口。從名字可以看出,它是一個pull解析者,并能獲取所有屬性集合。至于它的實現,我們先放一放。我們再來看看inflate的第二個重構方法,它由三個參數,第一個是XML解析者,第二個是這個布局所要加入的根View,那么第三個參數是什么呢?從面我們得知它是根據root!=null的結果得出,也就是如果我們調用第一個方法,root存在,它即為true,但第二個方法我們即可傳遞root,又能人為的把該值設置為false。其實從名字我們可以看出,這個參數是決定我們XML布局是否依附到root下,也就是是否add。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {//從parser對象中獲取AttributeSet的實例對象,猜測其中包含了所有的view的屬性final AttributeSet attrs = Xml.asAttributeSet(parser);mConstructorArgs[0] = mContext;//拿到了contextView result = root;//把返回結果默認設置為root。try {int type;while ((type = parser.next()) != XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {}//既不是開始標簽,又不是XML樹的結束點,那么就不是View對象了,不做任何處理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,但是沒有父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);//拿到第一個標簽后,就把parser對象又交出去了。} else {// 創建rootViewView temp = createViewFromTag(name, attrs);//根據標簽名創建一個view,因為是第一個標簽,所以是這個XML的根布局。ViewGroup.LayoutParams params = null;if (root != null) {// 根據父view來獲取一個匹配的paramsparams = root.generateLayoutParams(attrs);if (!attachToRoot) {//不需要添加到父view中,那么就給他設置一個臨時的params吧//設置一個臨時的paramstemp.setLayoutParams(params);}}rInflate(parser, temp, attrs);//把剩余的parser交出去處理。//如果給出了root,并且允許添加到父view中,就直接添加。if (root != null && attachToRoot) {root.addView(temp, params);}//如果根view為空,或者不允許加到父view中,就返回這個view,否則就返回rootif (root == null || !attachToRoot) {result = temp;}}} ...//異常的捕獲return result;} }從上面的代碼看,填充所有子view的過程都交給了rInflate,我們再來看看該方法的實現。
該方法會由上至下遞歸的初始化所有子view和子view的子view。在此方法被調用完成后 會調用此view的父view的onFinishInflate方法。表明其子view全部加載完畢。
private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs) throws XmlPullParserException, IOException { //用于記錄XML的深度,如果當前的parser處于根view,則深度為0,每進入一層,深度+1,每退出一層,深度-1。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)) {//這是獲取焦點的標簽,把它的父標簽設置為焦點 parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) { if (parser.getDepth() == 0) {//當深度為0,即根標簽等于include的時候,拋出異常。 throw new InflateException("<include /> cannot be the root element"); } parseInclude(parser, parent, attrs);//解析include的類容 } else if (TAG_MERGE.equals(name)) {//因為這是一個填充子view的過程,肯定不是root了,不符合merge的用法。 throw new InflateException("<merge /> must be the root element"); } else { //看這里,創建view的方法。而且這里已經重新獲得了它的 final View view = createViewFromTag(name, attrs); //和上面一樣,拿到標簽名后就可以創建view了final ViewGroup viewGroup = (ViewGroup) parent; //拿到它的父viewfinal ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs); //調用方法本身判斷它有沒有子view,進行遞歸viewGroup.addView(view, params); //遞歸完畢后把自身添加進去} } parent.onFinishInflate(); //遞歸調用完畢后,通知它的父view說自身已經填充完畢。 }我們再來看看createViewFromTag,根據一個名字創建一個view。
View createViewFromTag(String name, AttributeSet attrs) { if (name.equals("view")) { //如果名字是view的情況,給view重新賦值,可以針對這種情況:如 <View class="com.lipan.view"></View> name = attrs.getAttributeValue(null, "class"); } try {//mFactory是一個抽象的接口,該接口的實例對象在初始化LayoutInflate的時候,被賦值。View view = (mFactory == null) ? null : mFactory.onCreateView(name, mContext, attrs); if (view == null) { //如果返回的view等于null,不存在factory或者factory創建失敗。if (-1 == name.indexOf('.')) { //這里只是為了判斷xml文件中tag的屬性是否加了包名,不包含包名,則創建系統View view = onCreateView(name, attrs); } else { //創建自定義的Viewview = createView(name, null, attrs); } } return view; } ...//異常捕捉 }在上面我們還是沒看到一個View的真正創建,而只是看到分別調用了onCreateView和createView。
創建系統的View用了onCreateView,創建自定義View用了createView。我們先來看看onCreateView。
protected View onCreateView(String name, AttributeSet attrs)throws ClassNotFoundException {return createView(name, "android.view.", attrs); }發現該方法直接調用了createView,并且把包名默認為”android.view.”,那么createView就是真正的創建一個View了。
該方法接受三個參數,view名,view類的包名,以及view的屬性。
明白了View的創建,我們再來看看XML布局是如何被解析的吧,從上面我們可知XmlResourceParser是從Resource類的getLayout方法獲得的。
public XmlResourceParser getLayout(int id) throws NotFoundException { return loadXmlResourceParser(id, "layout"); //傳進了"layout",意思是說去找layout下的,可以用于區分R文件中的id } XmlResourceParser loadXmlResourceParser(int id, String type) throws NotFoundException { synchronized (mTmpValue) { //TypedValue對象是安卓提供的用于保存一個數據的容器,它的特別之處在于不僅可以保存數據,還能同時保存數據的類型。//例如該數據時boolean型的還是String類型的,甚至是某個對象的引用類型。這些在attr.xml文件下都有定義。TypedValue value = mTmpValue; getValue(id, value, true); //該方法只用于查找id的控件,如果找到,value對其進行引用,如果沒找到,拋出異常if (value.type == TypedValue.TYPE_STRING) { return loadXmlResourceParser(value.string.toString(), id, value.assetCookie, type); } throw new NotFoundException( "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + Integer.toHexString(value.type) + " is not valid"); } }根據上面的線索,我們先來看看getValue的實現:
/*getValue方法,id表示要查找的控件的 id,outValue是一個對象,用于保存一些屬性相關信息 resolveRefs為true表明,當通過屬性id找到xml文件中的標簽時,比如是一個<Button android:id="@+id/button"/> 它的值是一個引用,則繼續解析獲得這個id的值。這里看AssetManager類的實現*/ public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { boolean found = mAssets.getResourceValue(id, outValue, resolveRefs); if (found) { return; } throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)); }我們看到上面的方法把value交給了loadXmlResourceParser來處理,那么我們看看是如何處理的
XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws NotFoundException { if (id != 0) { try { //取緩存 synchronized (mCachedXmlBlockIds) { //首先在緩存集合中查看 final int num = mCachedXmlBlockIds.length; //可以從源碼中看出它是一個4固定長度的數組,用于緩存4個idfor (int i=0; i<num; i++) { if (mCachedXmlBlockIds[i] == id) { //找到了,直接newParser返回return mCachedXmlBlocks[i].newParser(); //是一個用于緩存XmlBlock的數組,長度也是4} } //第一次加載時,會打開這個文件獲取一個xml數據塊對象。 // 這里先看AssetManager類的實現 XmlBlock block = mAssets.openXmlBlockAsset( assetCookie, file); //下面會把此xmlBlock對象緩存起來,保存id和block, //以后如果是同樣的id,直接在緩存中取XmlBlock。 //這樣就不用再在本地方法中打開文件創建解析樹了。 if (block != null) { int pos = mLastCachedXmlBlockIndex+1; if (pos >= num) pos = 0; //如果角標超了,就直接替換第一個mLastCachedXmlBlockIndex = pos; XmlBlock oldBlock = mCachedXmlBlocks[pos]; if (oldBlock != null) { oldBlock.close(); } mCachedXmlBlockIds[pos] = id; mCachedXmlBlocks[pos] = block; //返回的內部類繼承了XmlResourceParser,在APi中此類是隱藏的 return block.newParser(); } } } ...//異常的處理}從上面可以看出,安卓對XML的最終解析是通過JNI調用的本地方法,由于我不懂C,能力有限,所以追索到此為止,但是大體思想我們應該也能明白,如果不考慮效率問題,其實我們也可以自己用pull解析來對XML文件解析,寫出自己的布局填充器,解析自定義的XML文件,或者是jason格式的布局,甚至任意與格式的布局,因為布局填充器是你寫的,你能解讀它。
總結
以上是生活随笔為你收集整理的Android XML 实例化的过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Activity、View、Window
- 下一篇: Android的跨进程通信