1. 程式人生 > >示例:Android註解實現程式碼注入

示例:Android註解實現程式碼注入

前面的部落格Android中的註解中,
我們簡單描述了Android中註解的含義和用途。

除了基本的用法外,註解還可以幫助我們實現程式碼注入,達到類似IoC的效果。
本篇部落格以一個簡單的例子,記錄一下相關的內容。

通常的情況下,我們初始化介面的程式碼類似於:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button mBtn1;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super
.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBtn1 = findViewById(R.id.test); mBtn1.setOnClickListener(this); } @Override public void onClick(View view) { switch (view.getId()) { case R.id.test: Log.d("ZJTest"
, "click test btn"); break; default: break; } } }

在上面程式碼的基礎上,我們看看如何使用註解來實現程式碼注入。

一、注入ContentView
首先,我們來簡單地替換掉setContentView方法。
定義一個註解:

/**
 * @author zhangjian on 18-3-16.
 */

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

其中,@Target表示該註解可以用於什麼地方,其定義如下:

    public enum ElementType {  
        /** 
         * Class, interface or enum declaration. 
         */  
        TYPE,  
        /** 
         * Field declaration. 
         */  
        FIELD,  
        /** 
         * Method declaration. 
         */  
        METHOD,  
        /** 
         * Parameter declaration. 
         */  
        PARAMETER,  
        /** 
         * Constructor declaration. 
         */  
        CONSTRUCTOR,  
        /** 
         * Local variable declaration. 
         */  
        LOCAL_VARIABLE,  
        /** 
         * Annotation type declaration. 
         */  
        ANNOTATION_TYPE,  
        /** 
         * Package declaration. 
         */  
        PACKAGE  
    }  

@Retention表示:表示需要在什麼級別儲存該註解資訊,其定義類似於:

    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  
    }  

定義完該註解後,我們就可以這樣使用了:

//ContentView註解修飾類,其值為R.layout.activity_main
@ContentView(value = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    .............
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //注入程式碼
        ViewInjectUtils.inject(this);
    }
    .............
}

public class ViewInjectUtils {
    public static void inject(Activity activity) {
        injectContentView(activity);
        ..............
    }

    private static void injectContentView(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        //獲取註解
        ContentView contentView = clazz.getAnnotation(ContentView.class);
        if (contentView != null) {
            //註解的值就是layout id
            int contentViewId = contentView.value();
            try {
                activity.setContentView(contentViewId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

單純從上面的程式碼來看,註解注入程式碼實際上沒有任何的優勢。
這裡重要的是瞭解使用方法。

二、注入普通View
現在我們再定義一個註解:

/**
 * @author zhangjian on 18-3-16.
 */

//這是修飾成員變數的
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
    int value();
}

定義該註解後,就可以這麼使用了:

/**
 * @author zhangjian
 */
@ContentView(value = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    //修飾成員變數
    @ViewInject(R.id.test)
    private Button mBtn1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewInjectUtils.inject(this);
    }

    ..........
}

/**
 * @author zhangjian on 18-3-16.
 */

public class ViewInjectUtils {
    public static void inject(Activity activity) {
        injectContentView(activity);
        injectViews(activity);
        ...........
    }
    .............
    private static void injectViews(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();

        //遍歷所有field,找出其中具有ViewInject註解的
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            ViewInject viewInject = field.getAnnotation(ViewInject.class);

            if (viewInject != null) {
                //取出值
                int viewId = viewInject.value();
                if (viewId != -1) {
                    try {
                        //找到view並賦值
                        Object view = activity.findViewById(viewId);
                        field.setAccessible(true);
                        field.set(activity, view);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    .........
}

有了ViewInject註解後,定義新的View時,只需要標上註解即可,
省略了每次呼叫findViewById。
不過從效率來看,這仍然沒有實際呼叫效率高。
還是那句話,重在體會這種思想。

三、注入事件監聽
實現了View的注入後,我們看看如何來通過注入完成對View點選事件的監聽。

我們先定義一個註解:

/**
 * @author zhangjian on 18-3-16.
 */

//這個註解是用來修飾註解的
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
    String setListenerMethod();
    Class<?> listenerClass();
    String listenerCallback();
}

基於EventBase再來定義一個註解:

/**
 * @author zhangjian on 18-3-16.
 */
//修飾method的
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//定義該註解對應的描述
@EventBase(setListenerMethod = "setOnClickListener",
        listenerClass = View.OnClickListener.class, listenerCallback = "onClick")
public @interface OnClick {
    int[] value();
}

有了上述註解後,我們之前的demo就可以修改為:

/**
 * @author zhangjian
 */
@ContentView(value = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    @ViewInject(R.id.test)
    private Button mBtn1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewInjectUtils.inject(this);
    }

    //我們的目前就是實現,點選R.id.test對應的button後
    //會呼叫到clickBtnInvoked函式
    @OnClick({R.id.test})
    public void clickBtnInvoked(View view) {
        switch (view.getId()) {
            case R.id.test:
                Log.d("ZJTest", "click test btn");
                break;
            default:
                break;
        }
    }
}

/**
 * @author zhangjian on 18-3-16.
 */

public class ViewInjectUtils {
    public static void inject(Activity activity) {
        injectContentView(activity);
        injectViews(activity);
        injectEvents(activity);
    }
    .............
    private static void injectEvents(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        Method[] methods = clazz.getMethods();

        //遍歷所有的method
        for (Method method : methods) {
            //獲取每個method對應的註解
            Annotation[] annotations = method.getAnnotations();

            for (Annotation annotation : annotations) {
                //獲取註解對應的型別
                Class<? extends Annotation> annotationType = annotation.annotationType();

                //若是EventBase修飾的註解,則需要注入事件監聽
                EventBase eventBase = annotationType.getAnnotation(EventBase.class);
                if (eventBase != null) {
                    //class name
                    Class<?> listenerType = eventBase.listenerClass();

                    //class method
                    String listenerSetter = eventBase.setListenerMethod();

                    //callback method
                    String methodName = eventBase.listenerCallback();

                    try {
                        //獲取註解對應的值
                        Method annotationMethod = annotationType.getDeclaredMethod("value");
                        int[] viewIds = (int[])annotationMethod.invoke(annotation);

                        //建立動態代理
                        DynamicHandler handler = new DynamicHandler(activity);
                        //關聯listener的回撥介面和註解實際修飾的method
                        handler.addMethod(methodName, method);
                        Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(),
                                new Class<?>[] {listenerType}, handler);

                        //為每一個View設定listener
                        for (int viewId : viewIds) {
                            View view = activity.findViewById(viewId);
                            Method setListenerMethod = view.getClass()
                                    .getMethod(listenerSetter, listenerType);
                            setListenerMethod.invoke(view, listener);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

至此,事件注入實現完畢。
其中,唯一有疑點就是生成動態代理的程式碼。

/**
 * @author zhangjian on 18-3-16.
 */

public class DynamicHandler implements InvocationHandler {
    private WeakReference<Object> mHandlerRef;
    private final HashMap<String, Method> mMethodMap = new HashMap<>(1);

    public DynamicHandler(Object handler) {
        mHandlerRef = new WeakReference<>(handler);
    }

    //完成關聯
    public void addMethod(String name, Method method) {
        mMethodMap.put(name, method);
    }

    public Object getHandler() {
        return mHandlerRef.get();
    }

    public void setHandler(Object handler) {
        mHandlerRef = new WeakReference<>(handler);
    }

    //這個函式其實就能很好的反映動態代理的功能
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        Object handler = mHandlerRef.get();
        if (handler != null) {
            //呼叫實際的方法
            String methodName = method.getName();
            Method realM = mMethodMap.get(methodName);
            if (realM != null) {
                return realM.invoke(handler, objects);
            }
        }

        return null;
    }
}

四、總結
至此,通過註解完成程式碼注入的示例介紹完畢。
雖然從上述例子來看,註解的優勢似乎並不明顯;
但這裡重點在於記錄一種思路,
為分析和使用retrofit等開源庫打下基礎。