Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android XML中引用自定義內部類view的四個why,androidxml

Android XML中引用自定義內部類view的四個why,androidxml

編輯:關於android開發

Android XML中引用自定義內部類view的四個why,androidxml


  今天碰到了在XML中應用以內部類形式定義的自定義view,結果遇到了一些坑。雖然通過看了一些前輩寫的文章解決了這個問題,但是我看到的幾篇都沒有完整說清楚why,於是決定做這個總結。

使用自定義內部類view的規則

  本文主要是總結why,所以先把XML布局文件中引用內部類的自定義view的做法擺出來,有四點:

布局加載流程主要代碼  

  首先,XML布局文件的加載都是使用LayoutInflater來實現的,通過這篇文章的分析,我們知道實際使用的LayoutInflater類是其子類PhoneLayoutInflater,然後通過這篇文章的分析,我們知道view的真正實例化的關鍵入口函數是createViewFromTag這個函數,然後通過createView來真正實例化view,下面便是該流程用到的關鍵函數的主要代碼:

  1     final Object[] mConstructorArgs = new Object[2];
  2 
  3     static final Class<?>[] mConstructorSignature = new Class[] {
  4             Context.class, AttributeSet.class};
  5             
  6     //...
  7     
  8     /**
  9      * Creates a view from a tag name using the supplied attribute set.
 10      * <p>
 11      * <strong>Note:</strong> Default visibility so the BridgeInflater can
 12      * override it.
 13      *
 14      * @param parent the parent view, used to inflate layout params
 15      * @param name the name of the XML tag used to define the view
 16      * @param context the inflation context for the view, typically the
 17      *                {@code parent} or base layout inflater context
 18      * @param attrs the attribute set for the XML tag used to define the view
 19      * @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
 20      *                        attribute (if set) for the view being inflated,
 21      *                        {@code false} otherwise
 22      */
 23     View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
 24             boolean ignoreThemeAttr) {
 25         //**關鍵1**//
 26         if (name.equals("view")) {
 27             name = attrs.getAttributeValue(null, "class");
 28         }
 29 
 30         // Apply a theme wrapper, if allowed and one is specified.
 31         if (!ignoreThemeAttr) {
 32             final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
 33             final int themeResId = ta.getResourceId(0, 0);
 34             if (themeResId != 0) {
 35                 context = new ContextThemeWrapper(context, themeResId);
 36             }
 37             ta.recycle();
 38         }
 39 
 40         if (name.equals(TAG_1995)) {
 41             // Let's party like it's 1995!
 42             return new BlinkLayout(context, attrs);
 43         }
 44 
 45         try {
 46             View view;
 47             if (mFactory2 != null) {
 48                 view = mFactory2.onCreateView(parent, name, context, attrs);
 49             } else if (mFactory != null) {
 50                 view = mFactory.onCreateView(name, context, attrs);
 51             } else {
 52                 view = null;
 53             }
 54 
 55             if (view == null && mPrivateFactory != null) {
 56                 view = mPrivateFactory.onCreateView(parent, name, context, attrs);
 57             }
 58 
 59             if (view == null) {
 60                 final Object lastContext = mConstructorArgs[0];
 61                 mConstructorArgs[0] = context;
 62                 try {
 63                     if (-1 == name.indexOf('.')) {
 64                         //**關鍵2**//
 65                         view = onCreateView(parent, name, attrs);
 66                     } else {
 67                         //**關鍵3**//
 68                         view = createView(name, null, attrs);
 69                     }
 70                 } finally {
 71                     mConstructorArgs[0] = lastContext;
 72                 }
 73             }
 74 
 75             return view;
 76         }
 77         //後面都是catch,省略
 78     }
 79     
 80     protected View onCreateView(View parent, String name, AttributeSet attrs)
 81             throws ClassNotFoundException {
 82         return onCreateView(name, attrs);
 83     }
 84     
 85     protected View onCreateView(String name, AttributeSet attrs)
 86             throws ClassNotFoundException {
 87         return createView(name, "android.view.", attrs);
 88     }
 89     
 90     /**
 91      * Low-level function for instantiating a view by name. This attempts to
 92      * instantiate a view class of the given <var>name</var> found in this
 93      * LayoutInflater's ClassLoader.
 94      * 
 95      * @param name The full name of the class to be instantiated.
 96      * @param attrs The XML attributes supplied for this instance.
 97      * 
 98      * @return View The newly instantiated view, or null.
 99      */
100     public final View createView(String name, String prefix, AttributeSet attrs)
101             throws ClassNotFoundException, InflateException {
102         Constructor<? extends View> constructor = sConstructorMap.get(name);
103         Class<? extends View> clazz = null;
104 
105         try {
106             Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
107 
108             if (constructor == null) {
109                 //**關鍵4**//
110                 // Class not found in the cache, see if it's real, and try to add it
111                 clazz = mContext.getClassLoader().loadClass(
112                         prefix != null ? (prefix + name) : name).asSubclass(View.class);
113                 
114                 if (mFilter != null && clazz != null) {
115                     boolean allowed = mFilter.onLoadClass(clazz);
116                     if (!allowed) {
117                         failNotAllowed(name, prefix, attrs);
118                     }
119                 }
120                 constructor = clazz.getConstructor(mConstructorSignature);
121                 constructor.setAccessible(true);
122                 sConstructorMap.put(name, constructor);
123             } else {
124                 // If we have a filter, apply it to cached constructor
125                 if (mFilter != null) {
126                     // Have we seen this name before?
127                     Boolean allowedState = mFilterMap.get(name);
128                     if (allowedState == null) {
129                         // New class -- remember whether it is allowed
130                         clazz = mContext.getClassLoader().loadClass(
131                                 prefix != null ? (prefix + name) : name).asSubclass(View.class);
132                         
133                         boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
134                         mFilterMap.put(name, allowed);
135                         if (!allowed) {
136                             failNotAllowed(name, prefix, attrs);
137                         }
138                     } else if (allowedState.equals(Boolean.FALSE)) {
139                         failNotAllowed(name, prefix, attrs);
140                     }
141                 }
142             }
143 
144             Object[] args = mConstructorArgs;
145             args[1] = attrs;
146             //**關鍵5**//
147             final View view = constructor.newInstance(args);
148             if (view instanceof ViewStub) {
149                 // Use the same context when inflating ViewStub later.
150                 final ViewStub viewStub = (ViewStub) view;
151                 viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
152             }
153             return view;
154 
155         }
156         //後面都是catch以及finally處理,省略
157     }

  PhoneLayoutInflater中用到的主要代碼:

 1 public class PhoneLayoutInflater extends LayoutInflater {  
 2     private static final String[] sClassPrefixList = {  
 3         "android.widget.",  
 4         "android.webkit.",  
 5         "android.app."  
 6     };  
 7     //......
 8     
 9     /** Override onCreateView to instantiate names that correspond to the 
10         widgets known to the Widget factory. If we don't find a match, 
11         call through to our super class. 
12     */  
13     @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {  
14         //**關鍵6**//
15         for (String prefix : sClassPrefixList) {  
16             try {  
17                 View view = createView(name, prefix, attrs);  
18                 if (view != null) {  
19                     return view;  
20                 }  
21             } catch (ClassNotFoundException e) {  
22                 // In this case we want to let the base class take a crack  
23                 // at it.  
24             }  
25         }  
26   
27         return super.onCreateView(name, attrs);  
28     }  
29 
30     //.........
31 }

WHY

  對於任何一個XML中的元素,首先都是從“1”處開始(“1”即表示代碼中標注“關鍵1”的位置,後同),下面便跟著代碼的流程來一遍:

static final Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class};

在Oracle的文檔上找到getConstructor函數的說明:

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返回的構造函數是其參數與傳getConstructor的參數完全匹配的那一個,如果沒有就拋出NoSuchMethodException異常。於是我們就知道,必須要有一個與mConstructorSignature完全匹配,就需要Context和AttributeSet兩個參數的構造函數;(2)要是該類表示的是非靜態的內部類,那麼應該把一個外部類實例作為第一個參數傳入。而我們的代碼中傳入的mConstructorSignature是不含有外部類實例的,因此我們必須把我們自定義的內部類view聲明為靜態的才不會報錯。有些同學的解釋只是說到了非靜態內部類需要由一個外部類實例來引用,但我想那萬一系統在加載的時候它會自動構造一個外部類實例呢?於是這裡給了否定的答案。

6. 拿到了類信息clazz之後,代碼來到了“5”,在這裡注意下mConstructorArgs,在貼出的代碼最前面有給出它的定義,為Object[2],並且,通過源碼中發現,mConstructorArgs[0]被賦值為創建LayoutInflater時傳入的context。於是我們就知道         在“5”這裡,傳給newInstance的參數為Context和AttributeSet。然後在Oracl的文檔上關於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; 我們這裡也沒有傳入外部類實例,也就是說對於靜態內部類也會報錯。這裡同樣驗證了自定義了的內部類view必須聲明為靜態類這個問題。

  至此,我們已經在假設使用“view”,再次強調是小寫‘v’,作為元素名稱的情況,推出了要在XML中應用自定義的內部類view必須滿足的三點條件,即在class屬性中使用“$”連接外部類和內部類,必須有一個接受Context和AttributeSet參數的構造函數,必須聲明為靜態的這三個要求。

  而如果不使用“view”標簽的形式來使用自定義內部類view,那麼在寫XML的時候我們發現,只能使用比如<com.willhua.MyClass.MyView />的形式,而不能使用<com.willhua.MyClass$MyView />的形式,這樣AndroidStudio會報“Tag start is not close”的錯。顯然,如果我們使用<com.willhua.MyClass.MyView />的形式,那麼在“關鍵4”處將會調用loadClass("com.willhua.MyClass.MyView"),這樣在前面也已經分析過,是不符合Java中關於內部類的完整命名規則的,將會報錯。有些人估計會粗心寫成大寫的V的形式,即<View class="com.willhua.MyClass$MyView" ... />的形式,這樣將會在運行時報“wrong type”錯,因為這樣本質上定義的是一個android.view.View,而你在代碼中卻以為是定義的com.willhua.MyClass$MyView。

  在標識的“2”處,該出的調用流程為onCreateView(parent, name, attrs)——>onCreateView(name, attrs)——>createView(name, "android.view.", attrs),在前面提到過,我們真正使用的的LayoutInflater是PhoneLayoutInflater,而在PhoneLayoutInflater對這個onCreateView(name, attrs)函數是進行了重寫的,在PhoneLayoutInflater的onCreateView函數中,即“6”處,該函數通過在name前面嘗試分別使用三個前綴:"android.widget.","android.webkit.","android.app."來調用createView,若都沒有找到則調用父類的onCreateView來嘗試添加"android.view."前綴來加載該view。所以,這也是為什麼我們可以比如直接使用<Button />, <View />的原因,因為這些常用都是包含在這四個包名裡的。

總結

至此,開篇提到的四個要求我們都已經找到其原因了。其實,整個流程走下來,我們發現,要使定義的元素被正確的加載到相關的類,有三種途徑

  為了搞清楚加載內部類view的問題,結果對整個加載流程都有了一定了解,感覺能說多若干個why了,還是收獲挺大的。

參考:

http://blog.csdn.net/gorgle/article/details/51428515

http://blog.csdn.net/abcdef314159/article/details/50921160

http://blog.csdn.net/abcdef314159/article/details/50925124

周末愉快~~~

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved