示例: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等开源库打下基础。

猜你喜欢

转载自blog.csdn.net/gaugamela/article/details/79609566
今日推荐