詳解Android XML中引用自定義內(nèi)部類view的四個why
今天碰到了在XML中應(yīng)用以內(nèi)部類形式定義的自定義view,結(jié)果遇到了一些坑。雖然通過看了一些前輩寫的文章解決了這個問題,但是我看到的幾篇都沒有完整說清楚why,于是決定做這個總結(jié)。
使用自定義內(nèi)部類view的規(guī)則
本文主要是總結(jié)why,所以先把XML布局文件中引用內(nèi)部類的自定義view的做法擺出來,有四點:
1.自定義的類必須是靜態(tài)類;
2.使用view作為XML文件中的tag,注意,v是小寫字母,小寫字母v,小寫字母v;
3.添加class屬性,注意,沒有帶android:命名空間的,表明該自定義view的完整路徑,且外部類與內(nèi)部類之間用美元“$”連接,而不是“.”,注意,要美元“$”,不要“.”;
4.自定義的view至少應(yīng)該含有帶有Context, AttributeSet這兩個參數(shù)的構(gòu)造函數(shù)
布局加載流程主要代碼
首先,XML布局文件的加載都是使用LayoutInflater來實現(xiàn)的,通過這篇文章的分析,我們知道實際使用的LayoutInflater類是其子類PhoneLayoutInflater,然后通過這篇文章的分析,我們知道view的真正實例化的關(guān)鍵入口函數(shù)是createViewFromTag這個函數(shù),然后通過createView來真正實例化view,下面便是該流程用到的關(guān)鍵函數(shù)的主要代碼:
final Object[] mConstructorArgs = new Object[2]; static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class}; //... /** * Creates a view from a tag name using the supplied attribute set. * <p> * <strong>Note:</strong> Default visibility so the BridgeInflater can * override it. * * @param parent the parent view, used to inflate layout params * @param name the name of the XML tag used to define the view * @param context the inflation context for the view, typically the * {@code parent} or base layout inflater context * @param attrs the attribute set for the XML tag used to define the view * @param ignoreThemeAttr {@code true} to ignore the {@code android:theme} * attribute (if set) for the view being inflated, * {@code false} otherwise */ View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { //**關(guān)鍵1**// if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } // Apply a theme wrapper, if allowed and one is specified. 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(); } if (name.equals(TAG_1995)) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); } try { View view; if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { //**關(guān)鍵2**// view = onCreateView(parent, name, attrs); } else { //**關(guān)鍵3**// view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } //后面都是catch,省略 } protected View onCreateView(View parent, String name, AttributeSet attrs) throws ClassNotFoundException { return onCreateView(name, attrs); } protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs); } /** * Low-level function for instantiating a view by name. This attempts to * instantiate a view class of the given <var>name</var> found in this * LayoutInflater's ClassLoader. * * @param name The full name of the class to be instantiated. * @param attrs The XML attributes supplied for this instance. * * @return View The newly instantiated view, or null. */ 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 { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { //**關(guān)鍵4**// // 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); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } 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); 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; args[1] = attrs; //**關(guān)鍵5**// final View view = constructor.newInstance(args); if (view instanceof ViewStub) { // Use the same context when inflating ViewStub later. final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } return view; } //后面都是catch以及finally處理,省略 }
PhoneLayoutInflater中用到的主要代碼:
public class PhoneLayoutInflater extends LayoutInflater { private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app." }; //...... /** Override onCreateView to instantiate names that correspond to the widgets known to the Widget factory. If we don't find a match, call through to our super class. */ @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { //**關(guān)鍵6**// for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack // at it. } } return super.onCreateView(name, attrs); } //......... }
WHY
對于任何一個XML中的元素,首先都是從“1”處開始(“1”即表示代碼中標(biāo)注“關(guān)鍵1”的位置,后同),下面便跟著代碼的流程來一遍:
1.“1”處進(jìn)行名字是否為“view”的判斷,那么這里會有兩種情況,我們先假設(shè)使用“view”
2.由于在“1”滿足條件,所以name被賦值為class屬性的值,比如“com.willhua.view.MyView”或者“com.willhua.view.MyClass$MyView”。其實這里也是要使用“view”來定義,而不是其他名字來定義的原因。
3.根據(jù)name的值,name中含有‘.'符號,于是代碼走到“3”處,調(diào)用createView,且prefix參數(shù)為空
4.來到“4”處,prefix為空,于是loadClass函數(shù)的參數(shù)即為name,即在a.2中說的class屬性的值。我們知道,傳給loadClass的參數(shù)是想要加載的類的類名,而在Java中,內(nèi)部類的類名表示都是在外部類類名的后面用符號“$”連接內(nèi)部類類名而來,于是文章開頭提到的第3點的答案也就是在這里了。補(bǔ)充一下,為什么是來到“4”,而不是對應(yīng)的else塊中呢?類第一次被加載的時候,構(gòu)造器肯定是還不存在的,也就是if條件肯定是成立的。然后等到后面再次實例化的時候,就來到了else塊中,而在else快中只是根據(jù)mFilter做一些是否可以加載該view的判斷而已,并沒有從本質(zhì)上影響view的加載流程。
5.在“4”處還有一個很重要的地方,那就是constructor = class.getConstructor(mConstructorSignature)這句。首先,在代碼開頭已經(jīng)給出了mConstructorSignature的定義:
static final Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class};
在Oracle的文檔上找到getConstructor函數(shù)的說明:
Returns a Constructor object that reflects the specified public constructor of the class represented by this Class object. The parameterTypes parameter is an array of Class objects that identify the constructor's formal parameter types, in declared order. If this Class object represents an inner class declared in a non-static context, the formal parameter types include the explicit enclosing instance as the first parameter.
The constructor to reflect is the public constructor of the class represented by this Class object whose formal parameter types match those specified by parameterTypes.
于是,這里其實解答了我們兩個問題:(1)getConstructor返回的構(gòu)造函數(shù)是其參數(shù)與傳getConstructor的參數(shù)完全匹配的那一個,如果沒有就拋出NoSuchMethodException異常。于是我們就知道,必須要有一個與mConstructorSignature完全匹配,就需要Context和AttributeSet兩個參數(shù)的構(gòu)造函數(shù);(2)要是該類表示的是非靜態(tài)的內(nèi)部類,那么應(yīng)該把一個外部類實例作為第一個參數(shù)傳入。而我們的代碼中傳入的mConstructorSignature是不含有外部類實例的,因此我們必須把我們自定義的內(nèi)部類view聲明為靜態(tài)的才不會報錯。有些同學(xué)的解釋只是說到了非靜態(tài)內(nèi)部類需要由一個外部類實例來引用,但我想那萬一系統(tǒng)在加載的時候它會自動構(gòu)造一個外部類實例呢?于是這里給了否定的答案。
6. 拿到了類信息clazz之后,代碼來到了“5”,在這里注意下mConstructorArgs,在貼出的代碼最前面有給出它的定義,為Object[2],并且,通過源碼中發(fā)現(xiàn),mConstructorArgs[0]被賦值為創(chuàng)建LayoutInflater時傳入的context。于是我們就知道 在“5”這里,傳給newInstance的參數(shù)為Context和AttributeSet。然后在Oracl的文檔上關(guān)于newInstance的說明中也有一句:If the constructor's declaring class is an inner class in a non-static context, the first argument to the constructor needs to be the enclosing instance; 我們這里也沒有傳入外部類實例,也就是說對于靜態(tài)內(nèi)部類也會報錯。這里同樣驗證了自定義了的內(nèi)部類view必須聲明為靜態(tài)類這個問題。
至此,我們已經(jīng)在假設(shè)使用“view”,再次強(qiáng)調(diào)是小寫‘v',作為元素名稱的情況,推出了要在XML中應(yīng)用自定義的內(nèi)部類view必須滿足的三點條件,即在class屬性中使用“$”連接外部類和內(nèi)部類,必須有一個接受Context和AttributeSet參數(shù)的構(gòu)造函數(shù),必須聲明為靜態(tài)的這三個要求。
而如果不使用“view”標(biāo)簽的形式來使用自定義內(nèi)部類view,那么在寫XML的時候我們發(fā)現(xiàn),只能使用比如<com.willhua.MyClass.MyView />的形式,而不能使用<com.willhua.MyClass$MyView />的形式,這樣AndroidStudio會報“Tag start is not close”的錯。顯然,如果我們使用<com.willhua.MyClass.MyView />的形式,那么在“關(guān)鍵4”處將會調(diào)用loadClass("com.willhua.MyClass.MyView"),這樣在前面也已經(jīng)分析過,是不符合Java中關(guān)于內(nèi)部類的完整命名規(guī)則的,將會報錯。有些人估計會粗心寫成大寫的V的形式,即<View class="com.willhua.MyClass$MyView" ... />的形式,這樣將會在運行時報“wrong type”錯,因為這樣本質(zhì)上定義的是一個android.view.View,而你在代碼中卻以為是定義的com.willhua.MyClass$MyView。
在標(biāo)識的“2”處,該出的調(diào)用流程為onCreateView(parent, name, attrs)——>onCreateView(name, attrs)——>createView(name, "android.view.", attrs),在前面提到過,我們真正使用的的LayoutInflater是PhoneLayoutInflater,而在PhoneLayoutInflater對這個onCreateView(name, attrs)函數(shù)是進(jìn)行了重寫的,在PhoneLayoutInflater的onCreateView函數(shù)中,即“6”處,該函數(shù)通過在name前面嘗試分別使用三個前綴:"android.widget.","android.webkit.","android.app."來調(diào)用createView,若都沒有找到則調(diào)用父類的onCreateView來嘗試添加"android.view."前綴來加載該view。所以,這也是為什么我們可以比如直接使用<Button />, <View />的原因,因為這些常用都是包含在這四個包名里的。
總結(jié)
至此,開篇提到的四個要求我們都已經(jīng)找到其原因了。其實,整個流程走下來,我們發(fā)現(xiàn),要使定義的元素被正確的加載到相關(guān)的類,有三種途徑:
1.只使用簡單類名,即<ViewName />的形式。對于“android.widget”, "android.webkit", "android.app"以及"android.view"這四個包內(nèi)的類,可以直接使用這樣的形式,因為在代碼中會自動給加上相應(yīng)的包名來構(gòu)成完整的路徑,而對于的其他的類,則不行,因為ViewName加上這四個包名構(gòu)成的完整類名都無法找到該類。
2.使用<"完整類名" />的形式,比如<com.willhua.MyView />或者<android.widget.Button />的形式來使用,對于任何的非內(nèi)部類view,這樣都是可以的。但是對于內(nèi)部類不行,因為這樣的類名不符合Java中關(guān)于內(nèi)部類完整類名的定義規(guī)則。如果<com.willhua.MyClass$MyView />的形式能夠通過編譯話,肯定是能正確Inflate該MyView的,可惜在編寫的時候就會提示錯。
3.使用<view class="完整類名" />的形式。這個是最通用的,所有的類都可以這么干,但是前提是“完整類名”寫對,比如前面提到的內(nèi)部類使用"$"符號連接。
為了搞清楚加載內(nèi)部類view的問題,結(jié)果對整個加載流程都有了一定了解,感覺能說多若干個why了,還是收獲挺大的。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android中的廣播、服務(wù)、數(shù)據(jù)庫、通知、包等術(shù)語的原理和介紹(圖解)
這篇文章主要介紹了Android中的廣播、服務(wù)、數(shù)據(jù)庫、通知、包等術(shù)語的原理和介紹(圖解),本文用圖文方式總結(jié)了Android中的一些開發(fā)術(shù)語,需要的朋友可以參考下2014-10-10詳解Android中的MVP架構(gòu)分解和實現(xiàn)
本篇文章主要介紹了詳解Android中的MVP架構(gòu)分解和實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02