背景
最近接觸新項目,項目中引入了Android Annotation(AA)依賴注入開源框架,代碼中大片的注解代碼,對於沒用過注解框架(或者說沒有如此大面積的使用)的我來說確實看得很費力,於是花時間研究了一下Android中的注解,當然了,這篇文章的目的並非講解AA的使用,而是主要講如何自定義注解以及用自定義的注解完成最常見的View Inject。
定義自己的注解
很簡單,new->Java Class->選擇Annotation,即完成了一個最基本的注解的定義。
public @interface TestAnnotation { }
順帶提一下,任何一個注解其實都是隱式地實現了java.lang.annotaion.Annotaion接口的。對於一個完備的自定義注解,我們通常還需要兩個東西,一是注解的修飾,二是注解的參數。
注解的修飾
我們可以用注解的注解(又叫元注解)來修飾注解,常見的有兩個:Retention和Target。
- Retention接收一個RetentionPolicy枚舉作為value,用以確定注解的保持策略,其源碼如下:
public enum RetentionPolicy { /** * Annotation is only available in the source code. */ SOURCE, /** * Annotation is available in the source code and in the class file, but not * at runtime. This is the default policy. */ CLASS, /** * Annotation is available in the source code, the class file and is * available at runtime. */ RUNTIME }
對於這三種類型,我的理解是這樣的:
- SOURCE作為最弱的一種策略,被標注為RetentionPolicy.SOURCE的注解,只是作為寫碼時的一種提醒,告訴我們某個特殊的含義,就像Android Studio中關鍵字、類名或者方法名的顏色區分,編譯器並不會讀取它。比如@Override注解,只是為了表明該方法是從父類中重寫的,即便我們注釋掉該注解,也不影響程序的編譯和運行。
- CLASS是缺省的保持策略,被標注為RetentionPolicy.CLASS的注解會影響到編譯過程,但不影響到運行時。比如@TargetApi,可以用他來屏蔽掉編譯時的非法版本API的錯誤,但是,如果你並沒有在該注解標注的代碼段內處理好新老版本的API使用問題的話,程序還是會在運行時發生異常。
- RUNTIME作為權限最高的策略,其保持策略會持續到運行時,因此可以使用反射來在運行時檢測注解的存在與否,接下來的Demo中使用到的就是這種方式。
- Target接收一個ElementType枚舉類型的數組作為參數,用以表示該注解的適用對象。常見的有FIELD、TYPE、METHOD、PARAMETER,顧名思義,不再贅述。
注解的參數
注解可以定義為需要多個參數,只要在使用該注解時按類型將實參傳入對應的形參即可,如果形參定義時有默認值則可不傳該參數,否則必須傳。如下所示:
public @interface TestAnnotation { String param1(); String param2() default "hello"; int param3(); }
使用時:
@TestAnnotation(param1 = "world", param3 = 1) void test() { }
當然,如果你的注解只需要一個參數,你可以把參數方法名定義成缺省的value()方法,這樣在傳參時就可以省略掉"value=<yourValue>"前面的"value="。
View Inject實現
前面提到,既然注解可以維持到進行時,那我們是不是也可以像一些注解框架那樣實現自己的視圖注解,以緩解頻繁的findViewbyId呢?首先想到的是定義一個ViewInject注解,該注解用於在標記某View會被自動findViewbyId,當然啦,我們需要一個傳入一個id,所以我們的ViewInject類看起來會像這樣:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ViewInject { int value(); }
使用時會像這樣:
@ViewInject(R.id.tv_inject) private TextView mTextView;
現在我們的注解定義和使用的完成了,我們該要解決如何完成我們的findViewById的動作的問題。這里我們開始用到一點反射的知識,首先我們通過反射獲取到當前類的Fields集合,然后遍歷集合,找出被標注為ViewInject注解的字段並拿出它們注解的value,也就是view在xml布局中的id,有了id我們就能為該字段獲取到它相應的view了。我們的代碼會像這樣:
Class cls = getClass(); Field[] fields = cls.getDeclaredFields(); for (Field field : fields) { if (!field.isAnnotationPresent(ViewInject.class)) continue; ViewInject viewInject = field.getAnnotation(ViewInject.class); if (viewInject.value() <= 0) continue; field.setAccessible(true); try { field.set(this, findViewById(viewInject.value())); } catch (IllegalAccessException e) { e.printStackTrace(); } }
為了增加重用性,減少重復代碼,我們很容易想到抽出一個BaseActivity,把這段代碼從子類的onCreate方法拿到父類BaseActivity的onCreate方法中,但是這樣問題就來了,如果進到父類的onCreate中時,子類還沒有setContentView怎么辦?沒有根布局咋findView啊,愁人。。咋辦呢,我們依葫蘆畫瓢,為Activity再定義一個注解並傳入layout id,這下只要我們在BaseActivity中先反射拿到layout id並setContentView,就再不用擔心沒有根布局了,於是我們的注入代碼進一步變成這樣:
Class cls = getClass(); // 整體布局注入 if (!cls.isAnnotationPresent(ActivityInject.class)) return; ActivityInject layoutInject = (ActivityInject) cls.getAnnotation(ActivityInject.class); if (layoutInject.value() <= 0) return; setContentView(layoutInject.value()); // View變量注入 Field[] fields = cls.getDeclaredFields(); for (Field field : fields) { if (!field.isAnnotationPresent(ViewInject.class)) continue; ViewInject viewInject = field.getAnnotation(ViewInject.class); if (viewInject.value() <= 0) continue; field.setAccessible(true); try { field.set(this, findViewById(viewInject.value())); } catch (IllegalAccessException e) { e.printStackTrace(); } }
到這里,我們的View Inject其實已經完成了,剩下的就屬於進階~如何進階呢,前面我們用到的都是FIED和TYPE級的注解來簡化findview代碼,接下來我們要用ElementType.METHOD來簡化OnCreate中后續操作的執行和簡化setOnClickListener的操作。新增兩個注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AfterCreate { }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface OnClick { int[] value(); }
對應的在BaseActivity中新增注入的解析:
Method[] methods = cls.getDeclaredMethods(); ArrayList<Method> afterMethods = new ArrayList<>(); for (final Method method : methods) { // 暫存AfterCreate方法 if (method.isAnnotationPresent(AfterCreate.class)) { afterMethods.add(method); } // 處理OnClick方法 if (method.isAnnotationPresent(OnClick.class)) { OnClick onClick = method.getAnnotation(OnClick.class); View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View v) { try { Class[] paramClasses = method.getParameterTypes(); // 這里只處理了無參數和只有一個view作參數的情況,其他情況不invoke if (paramClasses.length == 0) { method.invoke(BaseActivity.this, null); } else if (paramClasses.length == 1) { if (paramClasses[0].equals(View.class)) { method.invoke(BaseActivity.this, v); } } } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }; for (int id : onClick.value()) { if (id > 0) findViewById(id).setOnClickListener(onClickListener); } } } // 執行AfterCreate方法 for (Method method : afterMethods) { try { method.invoke(this, null); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
在子Activity中使用:
@AfterCreate void init() { mTextView.setText("Set TextView Content"); mImageView.setImageResource(R.mipmap.ic_launcher); } @OnClick({R.id.iv_inject,R.id.tv_inject}) void onClick(View view) { switch (view.getId()) { case R.id.iv_inject: Toast.makeText(getApplicationContext(), "ImageView is clicked!", Toast.LENGTH_SHORT).show(); break; case R.id.tv_inject: Toast.makeText(getApplicationContext(), "TextView is clicked!", Toast.LENGTH_SHORT).show(); break; } }
到這里就基本結束了。完整的Demo從這里下載。
