前面的博客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等开源库打下基础。