【Android】註解框架(二) 基礎知識(Java註解)& 執行時註解框架

指間沙似流年發表於2017-12-23

目錄

  1. 【Android】註解框架(一)-- 基礎知識Java 反射
  2. 【Android】註解框架(二)-- 基礎知識(Java註解)& 執行時註解框架
  3. 【Android】註解框架(三)-- 編譯時註解,手寫ButterKnife
  4. 【Android】註解框架(四)-- 一行程式碼注入微信支付

定義

註解是 JDK5 之後的新特性,是一種特殊的註釋,它為我們在程式碼中新增資訊提供了一種形式上的方法,使我們可以在稍後某個時候非常方便的使用這些資料。

Java內建的註解

JavaSE5內建了三種註解,定義在java.lang包中:

  1. @Override : 表示當前方法覆蓋超類中的方法。如果你所寫的方法和超類中的方法不同的話,編譯器會報錯。主要用於檢查。
  2. @Deprecated : 表明當前的元素已經不適用。當使用了註解為@Deprecated的元素時,編譯器會報出警告。
  3. @SuppressWarnings : 關閉不當的編譯器警告。

自定義註解

元註解

元註解主要用來註解定義的註解。目前主要有四種元註解。

  1. @Target

    表明當前註解可以使用在哪種元素上。 ElementType有以下幾種:

    • CONSTRUCTOR 構造器宣告
    • FIELD 域宣告(包括enum例項)
    • LOCAL_VARIABLE 區域性變數宣告
    • METHOD 方法宣告
    • PACKAGE 包宣告
    • PARAMETER 引數宣告
    • TYPE 類、介面、註解型別、enum型別
  2. @Retention

    表示需要在什麼級別儲存該註解資訊。

    • SOURCE 原始碼級別,註解將被編譯器丟棄,只存在原始碼中,其功能是與編譯器互動,用於程式碼檢測,如@Override,@SuppressWarings,許多框架如Dragger就是使用這個級別的註解,這個級別的框架額外效率損耗發生在編譯時。
    • CLASS 位元組碼級別,註解存在原始碼與位元組碼檔案中,主要用於編譯時生成而外的檔案,如XML,Java檔案等,這個級別需要新增JVM載入時候的代理(javaagent),使用代理來動態修改位元組碼檔案(由於Android虛擬機器並不支援所以本專題不會再做介紹,在Android中可以使用aspectJ來實現類似這個級別的功能)。
    • RUNTIME 執行時級別,註解存在原始碼,位元組碼與Java虛擬機器中,主要用於執行時反射獲取相關資訊,許多框架如OrmLite就是使用這個級別的註解,這個級別的框架額外的效率損耗發生在程式執行時。
  3. @Documented 被修飾的註解會生成到javadoc中。

  4. @Inherited 可以讓註解類似被繼承一樣,但是這並不是真的繼承。通過使用@Inherited,只可以讓子類類物件使用getAnnotations()反射獲取父類被@Inherited修飾的註解。

簡單例項 -- Android執行時註解

通常情況下,在Android開發中如果不使用類似於ButterKnife和XUtils之類的IOC框架的話,那麼就需要在程式碼中嵌入非常多的findViewById,那麼我們不通過第三方類庫,而是我們手動實現類似於XUtils的控制元件繫結的一個IOC框架,那麼我們在寫DEMO的時候就直接很方便的使用自己的程式碼了。

  1. 定義註解

    // 1.繫結控制元件註解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Bind {
        int value();
        int[] parentId() default 0;
    }
    // 2.檢查網路註解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface CheckNet {
    
    }
    // 3.繫結事件註解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface OnClick {
        int[] value();
        int[] parentId() default 0;
    }
    // 4. 繫結佈局
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface ContentView {
        int value();
    }
    複製程式碼
    • 定義註解的時候需要@interface
    • 註解引數的可支援資料型別:
      • 所有基本資料型別(int,float,boolean,byte,double,char,long,short)
      • String型別
      • Class型別
      • enum型別
      • Annotation型別
      • 以上所有型別的陣列
    • 引數的型別只能是public或者不寫兩種訪問修飾符
    • 如果註解沒有引數,那麼就和2中一樣,是一個空註解,用的時候直接標註
    • 如果註解只有一個引數,就和4中一樣,儘量使用value來表示引數,這樣在使用的時候可以直接@Bind(R.id.text_view),而不需要使用@Bind(value=R.id.text_view)
    • 如果註解的引數有預設值,可以參考3中的int[] parentId() default 0,在使用的時候如果不需要賦值可以不寫這個引數
    • 註解引數必須有確切的值,要麼在定義註解的預設值中指定,要麼在使用註解時指定,非基本型別的註解元素的值不可為null。因此, 使用空字串或0作為預設值是一種常用的做法。
  2. 解析註解

    我們以Activity為例

    1. 解析ContentView

      @Override
      public void inject(Activity activity) {
         Class<?> clazz = activity.getClass();
         // activity設定佈局
         try {
             ContentView contentView = findContentView(clazz);
             if (contentView != null) {
                 int layoutId = contentView.value();
                 if (layoutId > 0) {
                     Method setContentView = clazz.getMethod("setContentView", int.class);
                     setContentView.invoke(activity, layoutId);
                 }
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
         injectObject(activity, clazz, new ViewFinder(activity));
      }
      
      private static ContentView findContentView(Class<?> clazz) {
         return clazz != null ? clazz.getAnnotation(ContentView.class) : null;
      }
      複製程式碼

      通過反射來獲取類上的註解ContentView,如果有的話,在通過contentView.value()來獲取到設定在註解上的佈局Id,最後通過反射來將setContentView方法設定好。

    2. 繫結控制元件

      public static void injectObject(Object handler, Class<?> clazz, ViewFinder finder) {
         try {
             injectView(handler, clazz, finder);
             injectEvent(handler, clazz, finder);
         } catch (Exception e) {
             e.printStackTrace();
         }
      }
      
      private static void injectView(Object handler, Class<?> clazz, ViewFinder finder) throws Exception {
         // 獲取class的所有屬性
         Field[] fields = clazz.getDeclaredFields();
      
         // 遍歷並找到所有的Bind註解的屬性
         for (Field field : fields) {
             Bind viewById = field.getAnnotation(Bind.class);
             if (viewById != null) {
                 // 獲取View
                 View view = finder.findViewById(viewById.value(), viewById.parentId());
                 if (view != null) {
                     // 反射注入view
                     field.setAccessible(true);
                     field.set(handler, view);
                 } else {
                     throw new Exception("Invalid @Bind for "
                             + clazz.getSimpleName() + "." + field.getName());
                 }
             }
      
         }
      }
      複製程式碼

      和上面一樣,首先通過反射獲取到所有的Field引數,通過遍歷Field獲取到每個屬性上的註解,當獲取到的註解不為空的時候,就說明當前的屬性被Bind註解了,之後再獲取到View並通過field的set方法將view關聯到註解的引數上。

    3. 繫結事件

      private static void injectEvent(Object handler, Class<?> clazz, ViewFinder finder) throws Exception {
         // 獲取class所有的方法
         Method[] methods = clazz.getDeclaredMethods();
      
         // 遍歷找到onClick註解的方法
         for (Method method : methods) {
             OnClick onClick = method.getAnnotation(OnClick.class);
             boolean checkNet = method.getAnnotation(CheckNet.class) != null;
             if (onClick != null) {
                 // 獲取註解中的value值
                 int[] views = onClick.value();
                 int[] parentIds = onClick.parentId();
                 int parentLen = parentIds == null ? 0 : parentIds.length;
                 for (int i = 0; i < views.length; i++) {
                     // findViewById找到View
                     int viewId = views[i];
                     int parentId = parentLen > i ? parentIds[i] : 0;
                     View view = finder.findViewById(viewId, parentId);
                     if (view != null) {
                         // 設定setOnClickListener反射注入方法
                         view.setOnClickListener(new MyOnClickListener(method, handler, checkNet));
                     } else {
                         throw new Exception("Invalid @OnClick for "
                                 + clazz.getSimpleName() + "." + method.getName());
                     }
                 }
             }
         }
      }
      
      private static class MyOnClickListener implements View.OnClickListener {
         private Method method;
         private Object handler;
         private boolean checkNet;
      
         public MyOnClickListener(Method method, Object handler, boolean checkNet) {
             this.method = method;
             this.handler = handler;
             this.checkNet = checkNet;
         }
      
         @Override
         public void onClick(View v) {
             if (checkNet && !NetStateUtil.isNetworkConnected(v.getContext())) {
                 Toast.makeText(v.getContext(), "網路錯誤!", Toast.LENGTH_SHORT).show();
                 return;
             }
      
             // 注入方法
             try {
                 method.setAccessible(true);
                 method.invoke(handler, v);
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
      }
      複製程式碼

      繫結事件和繫結控制元件一樣,都是通過遍歷來獲取註解,再通過註解的引數來設定View的setOnClickListener。 這裡我們又通過另外一個註解CheckNet來判斷點選控制元件時候的引數,這樣就不需要每次在互動需要判斷網路的情況下寫判斷網路的程式碼了,直接一條@CheckNet就搞定。

  3. 使用註解

    // 繫結控制元件
    @Bind(R.id.viewpager)
    ViewPager viewpager;
    
    // 繫結事件並檢查網路
    @OnClick(R.id.dialog)
    @CheckNet
    void showDialog(TextView tv) {
    	Intent intent = new Intent(getActivity(), DialogViewActivity.class);
    	startActivity(intent);
    }
    複製程式碼

後記

通過自定義註解的學習,當我們需要使用的時候,可以通過自己來寫並擴充套件我們所需要的功能,這樣在使用的時候會非常方便,能夠在業務變動的時候能夠修正和擴充套件。

執行時IOC註解框架:github地址

相關文章