Android應用中使用執行時註解

linghu_java發表於2016-04-22

其實非常簡單,直接上程式碼:本文主要是替代傳統的findViewById()的功能,就是在我們Activity中不需要再使用findViewById()去給View賦值了,通過註解在執行階段自動賦值。以及setOnClickListener()也是一樣的原理。使用註解和反射技術。


1. 定義自己的annotation註解。

     定義findViewbyId這個功能的註解

package com.xxx.app.inject;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
   int value() default (int) -1;
}

      定義setOnclickListener的註解

package com.xxx.app.inject;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectClick {
   int[] value();
}
2. 定義自己的註解處理類:


package com.xxx.app.inject;

import android.app.Activity;
import android.view.View;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Injector {

   public static void injectView(Object obj, Object root) {
      Field[] fields = obj.getClass().getDeclaredFields();
      for (Field field : fields) {
         field.setAccessible(true);
         Annotation[] annotations = field.getAnnotations();
         if (annotations != null) {
            for (Annotation annotation : annotations) {
               if (annotation instanceof InjectView) {
                  InjectView injectView = (InjectView) annotation;
                  int value = injectView.value();
                  if (value != -1) {
                     try {
                        View view = getViewByRoot(root, value);
                        field.set(obj, view);
                     } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                     } catch (IllegalAccessException e) {
                        e.printStackTrace();
                     }
                  }
                  break;
               }
            }
         }
      }
   }

   public static void injectClick(Object obj, Object root) {
      Method[] methods = obj.getClass().getDeclaredMethods();
      for (Method method : methods) {
         Annotation[] annotations = method.getAnnotations();
         if (annotations != null) {
            for (Annotation annotation : annotations) {
               if (annotation instanceof InjectClick) {
                  InjectClick inject = (InjectClick) annotation;
                  int[] value = inject.value();
                  if (value != null && value.length > 0) {
                     View.OnClickListener listener = (View.OnClickListener) obj;
                     try {
                        for (int res : value) {
                           View view = getViewByRoot(root, res);
                           if (view == null) {
                              throw new NullPointerException();
                           }
                           view.setOnClickListener(listener);
                        }
                     } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                     }
                  }
               } else if (annotation instanceof InjectLongClick) {
                  InjectLongClick inject = (InjectLongClick) annotation;
                  int[] value = inject.value();
                  if (value != null && value.length > 0) {
                     View.OnLongClickListener listener = (View.OnLongClickListener) obj;
                     try {
                        for (int res : value) {
                           View view = getViewByRoot(root, res);
                           if (view == null) {
                              throw new NullPointerException();
                           }
                           view.setOnLongClickListener(listener);
                        }
                     } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                     }
                  }
               }
            }
         }
      }
   }

   public static View getViewByRoot(Object root, int res) {
      View view = null;
      if (root instanceof Activity) {
         view = ((Activity)root).findViewById(res);
      } 
      return view;
   }

}

3. Activity中使用註解:

@InjectView(R.id.action_back)
private ImageView actionBack;
@InjectView(R.id.site_top_bg)
private ImageView mSiteTopBg;
@InjectView(R.id.site_name)
private TextView mSiteName;
@InjectView(R.id.site_producter)
private TextView mProducter;
@InjectView(R.id.site_logo)
private ImageRoundView mSiteLogo;
@InjectView(R.id.site_description)
private TextView mSiteDescription;
@InjectView(R.id.viewPager)
private ViewPager mViewPager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_site);
    Injector.injectView(this, this);
    Injector.injectClick(this, this);

    
}
@Override
@InjectClick({R.id.action_back})
public void onClick(View v) {
    int id = v.getId();
    switch (id){
        case R.id.action_back:
            SiteDetailActivity.this.finish();
            break;
    }
}

=============================================


一、佈局檔案的註解 
我們在Android開發的時候,總是會寫到setContentView方法,為了避免每次都寫重複的程式碼,我們需要使用註解來代替我們做這個事情,只需要在類Activity上宣告一個ContentView註解和對應的佈局檔案就可以了。

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewUtils.injectContentView(this);
    }
}

從上面可以看到,上面程式碼在MainActivity上面使用到了ContentView註解,下面我們來看看ContentView註解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
    int value();
}

這個註解很簡單,它有一個int的value,用來存放佈局檔案的id,另外它註解的物件為一個型別,需要說明的是,註解的生命週期會一直到執行時,這個很重要,因為程式是在執行時進行反射的,我們來看看ViewUtils.injectContentView(this)方法,它進行的就是註解的處理,就是進行反射呼叫setContentView()方法。

    public static void injectContentView(Activity activity) {
        Class a = activity.getClass();
        if (a.isAnnotationPresent(ContentView.class)) {
            // 得到activity這個類的ContentView註解
            ContentView contentView = (ContentView) a.getAnnotation(ContentView.class);
            // 得到註解的值
            int layoutId = contentView.value();
            // 使用反射呼叫setContentView
            try {
                Method method = a.getMethod("setContentView", int.class);
                method.setAccessible(true);
                method.invoke(activity, layoutId);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

如果對Java註解比較熟悉的話,上面程式碼應該很容易看懂。

二、欄位的註解 
除了setContentView之外,還有一個也是我們在開發中必須寫的程式碼,就是findViewById,同樣,它也屬於簡單但沒有價值的編碼,我們也應該使用註解來代替我們做這個事情,就是對欄位進行註解。

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    @ViewInject(R.id.btn1)
    private Button mButton1;
    @ViewInject(R.id.btn2)
    private Button mButton2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewUtils.injectContentView(this);
        ViewUtils.injectViews(this);
    }
}

上面我們看到,使用ViewInject對兩個Button進行了註解,這樣我們就是不用寫findViewById方法,看上去很神奇,但其實原理很簡單。我們先來看看ViewInject註解。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
    int value();
}

這個註解也很簡單,就不說了,重點就是註解的處理了。

    public static void injectViews(Activity activity) {
        Class a = activity.getClass();
        // 得到activity所有欄位
        Field[] fields = a.getDeclaredFields();
        // 得到被ViewInject註解的欄位
        for (Field field : fields) {
            if (field.isAnnotationPresent(ViewInject.class)) {
                // 得到欄位的ViewInject註解
                ViewInject viewInject = field.getAnnotation(ViewInject.class);
                // 得到註解的值
                int viewId = viewInject.value();
                // 使用反射呼叫findViewById,併為欄位設定值
                try {
                    Method method = a.getMethod("findViewById", int.class);
                    method.setAccessible(true);
                    Object resView = method.invoke(activity, viewId);
                    field.setAccessible(true);
                    field.set(activity, resView);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

            }
        }
    }

上面的註釋很清楚,使用的也是反射呼叫findViewById函式。

三、事件的註解 
在Android開發中,我們也經常遇到setOnClickListener這樣的事件方法。同樣我們可以使用註解來減少我們的程式碼量。

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewUtils.injectContentView(this);
        ViewUtils.injectEvents(this);
    }

    @OnClick({R.id.btn1, R.id.btn2})
    public void clickBtnInvoked(View view) {
        switch (view.getId()) {
            case R.id.btn1:
                Toast.makeText(this, "Button1 OnClick", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn2:
                Toast.makeText(this, "Button2 OnClick", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

佈局檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:background="#70DBDB"
    android:orientation="vertical"
    tools:context="statusbartest.hpp.cn.statusbartest.MainActivity">
    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Test1"/>

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Test2"/>
</LinearLayout>

可以看到,上面我們沒有對Button呼叫setOnClickListener,但是當我們點選按鈕的時候,就會回撥clickBtnInvoked方法,這裡我們使用的就是註解來處理的。下面先來看看OnClick註解。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener", methodName = "onClick")
public @interface OnClick {
    int[] value();
}

可以看到這個註解使用了一個自定義的註解。

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
    Class listenerType();
    String listenerSetter();
    String methodName();
}

下面來看看註解的處理。

    public static void injectEvents(Activity activity) {
        Class a = activity.getClass();
        // 得到Activity的所有方法
        Method[] methods = a.getDeclaredMethods();
        for (Method method : methods) {
            // 得到被OnClick註解的方法
            if (method.isAnnotationPresent(OnClick.class)) {
                // 得到該方法的OnClick註解
                OnClick onClick = method.getAnnotation(OnClick.class);
                // 得到OnClick註解的值
                int[] viewIds = onClick.value();
                // 得到OnClick註解上的EventBase註解
                EventBase eventBase = onClick.annotationType().getAnnotation(EventBase.class);
                // 得到EventBase註解的值
                String listenerSetter = eventBase.listenerSetter();
                Class<?> listenerType = eventBase.listenerType();
                String methodName = eventBase.methodName();
                // 使用動態代理
                DynamicHandler handler = new DynamicHandler(activity);
                Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class<?>[] { listenerType }, handler);
                handler.addMethod(methodName, method);
                // 為每個view設定點選事件
                for (int viewId : viewIds) {
                    try {
                        Method findViewByIdMethod = a.getMethod("findViewById", int.class);
                        findViewByIdMethod.setAccessible(true);
                        View view  = (View) findViewByIdMethod.invoke(activity, viewId);
                        Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                        setEventListenerMethod.setAccessible(true);
                        setEventListenerMethod.invoke(view, listener);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

            }

        }
    }

這個程式碼相對上面的要複雜一些,它使用到了動態代理,關於動態代理的基本用法可以看看前面我提到的預備知識。

public class DynamicHandler implements InvocationHandler {
    private final HashMap<String, Method> methodMap = new HashMap<String, Method>(
            1);
    // 因為傳進來的為activity,使用弱引用主要是為了防止記憶體洩漏
    private WeakReference<Object> handlerRef;
    public DynamicHandler(Object object) {
        this.handlerRef = new WeakReference<Object>(object);
    }

    public void addMethod(String name, Method method) {
        methodMap.put(name, method);
    }
    // 當回到OnClickListener的OnClick方法的時候,它會呼叫這裡的invoke方法
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        // 得到activity例項
        Object handler = handlerRef.get();
        if (handler != null) {
            // method對應的就是回撥方法OnClick,得到方法名
            String methodName = method.getName();
            // 得到activtiy裡面的clickBtnInvoked方法
            method = methodMap.get(methodName);
            if (method != null) {
                // 回撥clickBtnInvoked方法
                return method.invoke(handler, objects);
            }
        }
        return null;
    }
}

基本的看註釋就應該差不多了。





相關文章