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

来源:http://www.cnblogs.com/willhua/archive/2016/12/04/6130155.html
-Advertisement-
Play Games

今天碰到了在XML中應用以內部類形式定義的自定義view,結果遇到了一些坑。雖然通過看了一些前輩寫的文章解決了這個問題,但是我看到的幾篇都沒有完整說清楚why,於是決定做這個總結。 使用自定義內部類view的規則 本文主要是總結why,所以先把XML佈局文件中引用內部類的自定義view的做法擺出來, ...


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

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

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

  1. 自定義的類必須是靜態類
  2. 使用view作為XML文件中的tag,註意,v是小寫字母,小寫字母v,小寫字母v;
  3. 添加class屬性,註意,沒有帶android:命名空間的,表明該自定義view的完整路徑,且外部類與內部類之間用美元“$”連接,而不是“.”,註意,要美元“$”,不要“.”;
  4. 自定義的view至少應該含有帶有Context, AttributeSet這兩個參數的構造函數

佈局載入流程主要代碼  

  首先,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”的位置,後同),下麵便跟著代碼的流程來一遍:

  1. “1”處進行名字是否為“view”的判斷,那麼這裡會有兩種情況,我們先假設使用“view”
  2. 由於在“1”滿足條件,所以name被賦值為class屬性的值,比如“com.willhua.view.MyView”或者“com.willhua.view.MyClass$MyView”。其實這裡也是要使用“view”來定義,而不是其他名字來定義的原因。
  3. 根據name的值,name中含有‘.’符號,於是代碼走到“3”處,調用createView,且prefix參數為空
  4. 來到“4”處,prefix為空,於是loadClass函數的參數即為name,即在a.2中說的class屬性的值。我們知道,傳給loadClass的參數是想要載入的類的類名,而在Java中,內部類的類名錶示都是在外部類類名的後面用符號“$”連接內部類類名而來,於是文章開頭提到的第3點的答案也就是在這裡了。補充一下,為什麼是來到“4”,而不是對應的else塊中呢?類第一次被載入的時候,構造器肯定是還不存在的,也就是if條件肯定是成立的。然後等到後面再次實例化的時候,就來到了else塊中,而在else快中只是根據mFilter做一些是否可以載入該view的判斷而已,並沒有從本質上影響view的載入流程。
  5. 在“4”處還有一個很重要的地方,那就是constructor = class.getConstructor(mConstructorSignature)這句。首先,在代碼開頭已經給出了mConstructorSignature的定義:
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 />的原因,因為這些常用都是包含在這四個包名里的。

總結

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

  1. 只使用簡單類名,即<ViewName />的形式。對於“android.widget”, "android.webkit", "android.app"以及"android.view"這四個包內的類,可以直接使用這樣的形式,因為在代碼中會自動給加上相應的包名來構成完整的路徑,而對於的其他的類,則不行,因為ViewName加上這四個包名構成的完整類名都無法找到該類。
  2. 使用<"完整類名" />的形式,比如<com.willhua.MyView />或者<android.widget.Button />的形式來使用,對於任何的非內部類view,這樣都是可以的。但是對於內部類不行,因為這樣的類名不符合Java中關於內部類完整類名的定義規則。如果<com.willhua.MyClass$MyView />的形式能夠通過編譯話,肯定是能正確Inflate該MyView的,可惜在編寫的時候就會提示錯。
  3. 使用<view class="完整類名" />的形式。這個是最通用的,所有的類都可以這麼乾,但是前提是“完整類名”寫對,比如前面提到的內部類使用"$"符號連接。

  為了搞清楚載入內部類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

周末愉快~~~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Python來做應用題及思路 最近找工作頭疼沒事就開始琢磨python解應用題應該可以,順便還可以整理下思路當然下麵的解法只是個人理解,也歡迎大佬們給意見或者指點更好的解決辦法等於優化代碼了嘛,也歡迎大家出點小題目做也可以,如果可以我也會定期專門來做應用題(你弟弟或者你表弟或者外甥等來問應用題在也不 ...
  • 英文文檔: __import__(name, globals=None, locals=None, fromlist=(), level=0) This function is invoked by the import statement. It can be replaced (by impor ...
  • 閑來無事,寫了一段通過類模板實現棧的代碼,分享給大家..... (關於棧的更多的詳細信息,詳見:http://www.cplusplus.com/reference/stack/stack/?kw=stack) 棧的聲明及實現 測試代碼 總結: 以上代碼,僅供個人娛樂. 在STL中的棧(stack) ...
  • 我遇到的問題 在做上位機軟體的時候,需要將上位軟體的命令傳輸到每個被控席位,也需要和被控電腦進行數據交換,我們的被控端是伺服器,也可能是客戶端,甚至有時候會遇到客戶端先啟動服務端後啟動情況,要控制的機器也可能是多台,同時我們要支持TCP和UDP兩種協議。 好酒加冰塊-交互過程 如果採用Tcp作為連 ...
  • 閱讀目錄 前言 明確業務細節 建模 實現 結語 一、前言 上一篇我們已經確立的購買上下文和銷售上下文的交互方式,傳送門在此:http://www.cnblogs.com/Zachary-Fan/p/DDD_6.html,本篇我們來實現售價上下文的具體細節。 二、明確業務細節 電商市場越來越成熟,競爭 ...
  • 前面寫過了使用ViewFlipper和ViewPager實現屏幕中視圖切換的效果(ViewPager未實現輪播)附鏈接: Android中使用ViewFlipper實現屏幕切換 Android中使用ViewPager實現屏幕頁面切換和頁面切換效果 今天我們在換一種實現方式ImageViewSwitc ...
  • 活動的啟動模式對我們來說是個新的概念,在實際項目中我們會根據活動的需求為每個活動指定恰當的啟動模式。共分為四種分別是:standard,singletop,singletast,singleinstance,可以在androidMainfest.xml中進得指定,android:launchMode... ...
  • 當運行下一個活動時,上一個活動被K掉了,當我們返回上一個活動時,系統會重啟create一個活動,問題來了我們之前在保存的數據怎麼辦?onSaveInstanceState可以用這個方法來進行保存,鍵值對[ke,"value"],其實和Intent一樣,也是通過這樣保存。 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...