在刚接触安卓的第二天 , 自己最熟悉的代码 , 就是那句findViewById. 记得当时特别舒服的啪啪啪敲完一行有用的代码 , 心里美滋儿啊 , 心里想着 , 啥时候写其他的逻辑就像这段啪啪啪就能完成的代码一样 , 那多舒服 。
但是吧 , 人都是有惰性的 , 我说的是偷懒的那种惰性:
在面对一个比较复杂的界面的时候 , 你需要机械化的吧所有的组件统统findViewById找出来 , 然后再去做相关操作 ; 有时候仅仅是为了设置一个点击事件 , 但却必须要先声明 , 查找 , 才能继续完成接下来的工作 .于是 , 我想偷个懒了.....
抽取方法
当然了,最先想到的,当然是想吧这些麻烦的重复性操作抽出来,其实也就是少写了findViewById的这么几个字,本质上还是和以前的逻辑一样:
protected final <T extends View> T $(@IdRes int id) {
return (T) view.findViewById(id);
}
这样,就可以吧代码简化为iv_head = $(R.id.iv_head);
emmm,好像意义不是很大,仍然是重复的工作。
IOC的出现
偶然的机会,接触到了ButterKnife的框架,这个框架极大地简化了组件查找,事件等一系列的操作,只需要一个注解就可以轻松搞定那些繁杂的工作。
@Bind(R.id.toolbar)
protected Toolbar toolbar;
这样,通过注解就可以完成定义到查找的两个工作,恩~舒服,但是这个注解的背后又存在着怎么样的实现呢?
略微看一下BK的源码吧
- 首先,我们点进去@Bind这个注解,来到了响应的注解接口
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
/** View ID to which the field will be bound. */
int[] value();
}
哇什么鬼,还有@Interface,是不是没见过的科技? 不要急 , 慢慢来看
这里一共有三个注解
Rentation: Reteniton的作用是定义被它所注解的注解保留多久,它的取值是一个枚举类型,有三种:
SOURCE 被编译器忽略
CLASS 注解将会被保留在Class文件中,但在运行时并不会被VM保留。这是默认行为
RUNTIME 保留至运行时。所以我们可以通过反射去获取注解信息。Target 用于设定注解使用范围,接收参数为ElementType的枚举
METHOD 可用于方法上
TYPE 可用于类或者接口上
ANNOTATION_TYPE 可用于注解类型上(被@interface修饰的类型)
CONSTRUCTOR 可用于构造方法上
FIELD 可用于域上
LOCAL_VARIABLE 可用于局部变量上
PACKAGE 用于记录java文件的package信息
PARAMETER 可用于参数上-
interface: 用于自定义注解
自定义注解也就是可以自己写需要的注解
- 使用@interface关键字定义注解,注意关键字的位置
- 成员以无参数无异常的方式声明,注意区别一般类成员变量的声明
- 可以使用default为成员指定一个默认值,如上所示
- 成员类型是受限的,合法的类型包括原始类型以及String、Class、Annotation、Enumeration (JAVA的基本数据类型有8种:byte(字节)、short(短整型)、int(整数型)、long(长整型)、float(单精度浮点数类型)、double(双精度浮点数类型)、char(字符类型)、boolean(布尔类型)
- 注解类可以没有成员,没有成员的注解称为标识注解
- 如果注解只有一个成员,并且把成员取名为value(),则在使用时可以忽略成员名和赋值号“=” ,例如JDK注解的@SuppviseWarnings ;如果成员名不为value,则使用时需指明成员名和赋值号"="
打造手撸的IOC框架
什么,才刚刚不到5分钟就可以开始手撸了么,别慌, 刚!
首先 , 我们模拟三种需求:
- 使用注解设置布局,代替setContentView
- 使用ViewInject代替findViewById
- 使用@OnClick和@OnLongClick代替点击与长按
IOC的本质就是控制反转 , 也就是将A要做的事情, 委托给B来做 , 所以我们需要一个第三方的容器类来完成这些工作
首先做好准备工作, 将BaseActivity搭建好 , 在里面调用容器的初始化 , 这样让每一个Activity去继承改基类就可以完成初始化的操作
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//将当前对象注入到第三方的容器
InjectUtils.inject(this);
increate(savedInstanceState);
}
public abstract void increate(Bundle savedInstanceState);
}
然后新建InjectUtils类 , 编写静态的Inject方法 , 这里传入上下文对象.
public static void inject(Context context) {
//先注入视图,再注入控件
injectLayout(context);
injectView(context);
injectEvents(context);
}
定义好三个方法之后 , 我们的基础框架就基本完成了 , 接下来实现一下具体的注解实现细节
任务1. 使用注解设置布局
@ConvertView(R.layout.activity_second)
首先 , 既然是自定义的注解 , 当然要先有一个类似于刚刚上面的自定义的@inteface , 然后默认的方法参数为int类型 , 代码如下:
//运行是也存在,用于注解
@Retention(RetentionPolicy.RUNTIME)
//用在类上的注解, 写在类的上方
@Target(ElementType.TYPE)
public @interface ConvertView {
int value();
}
定义好了注解之后 , 就要完成第三方容器中的方法了 , 这里我们使用反射的方法去找到注解, 然后反射到响应的方法 , 并调用方法本身 , 就完成了容器的作用 , 也就是上面准备的injectLayout方法
public static void injectLayout(Context context) {
int layoutId = 0;
Class clazz = context.getClass();
//从注解出拿到注解中的值
ConvertView view = (ConvertView) clazz.getAnnotation(ConvertView.class);
if (null != view) {
//从接口的value函数中获取id值
try {
layoutId = view.value();
//利用反射获取需要的方法
Method method = clazz.getMethod("setContentView", int.class);
//拿到setContentView后调用函数
method.invoke(context, layoutId);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
至此 , 我们完成了第一个需求 , 以后不再有setContentView, 只需要注解就可以了:
@ConvertView(R.layout.activity_second)
public class SecondActivity extends BaseActivity {
@Override
public void increate(Bundle savedInstanceState) {
}
}
任务2. 使用ViewInject代替findViewById
有了布局的经验之后 , 我们可以轻车熟路的按照流程来实现这个方法
首先是自定义的注解
//运行时也存在
@Retention(RetentionPolicy.RUNTIME)
//用在域上的注解
@Target(ElementType.FIELD)
public @interface ViewInject {
int value();
}
接下来在容器中实现委托的操作
public static void injectView(Context context) {
Class<? extends Context> aClass = context.getClass();
//获取到上下文中所有成员变量
Field[] declaredFields = aClass.getDeclaredFields();
for (Field f : declaredFields) {
//获取有注解的控件,注意注解之后没有加分号,意味着注解和类型声明是同一行语句 , 这里利用注解获取控件的本质是通过反射到成员变量
ViewInject annotation = f.getAnnotation(ViewInject.class);
//如果有注解 , 则获取注解的值
if (null != annotation) {
int value = annotation.value();
try {
//调用了Activity的findViewById方法,context中没有该方法,需要反射获取
Method findViewById = aClass.getMethod("findViewById", int.class);
View view = (View) findViewById.invoke(context, value);
//允许反射私有变量
f.setAccessible(true);
//为反射到的变量赋值
f.set(context, view);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
好像比设置布局要复杂一点?其实是差不多的 , 只不过这里在拿到方法之后, 有设置了返回值并实例化(改变字段) , 懂了第一个之后 , 也不是很难理解吧 .
任务3. 使用@OnClick和@OnLongClick代替点击与长按
这个相比于前两个就要复杂一些了 ,
- setContentView()只需要调用方法传入参数即可
- findViewById(), 需要传入参数并拿到返回值即可
- setOnclickListener()需要传入一个接口并实现其方法 .
纳尼?要传入一个方法,也就是View.OnclickListener的实现方法。我们知道这个方法是在View中的,这里难道还要传入一个View的参数么?没必要这么麻烦的。当我们需要访问某个对象但存在困难时,可以通过一个代理对象去间接的访问,所以就需要绕个小弯子: 使用代理模式。
在定义注解之前我们先想一下 , 如果要用注解编写点击事件的话 , 我们会省略掉一下几个步骤:
- 点击事件的方法 被省略
- 点击事件的参数: 被省略
- 匿名内部类的回调方法的方法回调 被省略
所以我们在注解中需要三个参数 。而且后期我们会添加各种各样的监听事件,所以在点击事件上做一个封装,用来管理所有的事件
@Retention(RetentionPolicy.RUNTIME)
//用于注解上的注解
@Target(ElementType.ANNOTATION_TYPE)
public @interface EventBase {
//设置监听方法
String listenerSetter();
//事件类型
Class listenerType();
//事件回调
String callBackMethod();
}
可以理解为注解的基类。我们的点击事件也好 , 长按事件也好 , 都基于该基类扩展 , 所以该基类会作为一个参数出现在另一个注解中 , 所以我们的@Target必须为ANNOTATION_TYPE , 也就是注解中的注解. 并且,将参数设置为String而不是Method,也是为了反射与扩展的方便。
首先我们编写OnClick注解,使用基类来传递参数:
@Retention(RetentionPolicy.RUNTIME)
//用于方法上的注解
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnClickListener",
listenerType = View.OnClickListener.class,
callBackMethod = "onClick")
public @interface OnClick {
int[] value() default -1;
}
现在明白基类的作用了吧,其实就是限制了参数的传递规范(这个规范使用枚举会更有可读性,这里就不浪了,大家自己来吧)。
敲重点,在委托容器中完成注入操作之前 , 我们先要编写代理 , 在代理中调用方法:
public class ListenerInvactionHandler implements InvocationHandler {
//被代理的真实对象的应用
private Context context;
//保存方法名与方法体, 用来判断是否需要被代理
private Map<String, Method> methodMap;
public ListenerInvactionHandler(Context context, Map<String, Method> methodMap) {
this.context = context;
this.methodMap = methodMap;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//首先获取到方法名
String name = method.getName();
//根据方法名找方法,看是否需要代理
Method metf = methodMap.get(name);
if (metf != null) {
//需要代理,使用代理调用
return metf.invoke(context, args);
} else {
return method.invoke(proxy, args);
}
}
}
还算好理解吧 , 上面这个方法中, 集合时用来保存类中所有的方法, 然后根据方法名来查看该方法是否需要被代理 , 如果需要的话使用代理来调用 , 否则使用本身来调用.
接下来是容器中的操作先贴上代码,在代码中吧重点都注释了.
@SuppressLint("NewApi")
private static void injectEvents(Context context) {
Class<? extends Context> aClass = context.getClass();
//拿到类中所有的方法
Method[] declaredMethods = aClass.getDeclaredMethods();
//遍历所有的方法并查找带注解的方法
for (Method m : declaredMethods) {
Annotation[] annotations = m.getAnnotations();
for (Annotation a : annotations) {
//获取注解 annoType:OnClick
Class<? extends Annotation> annoType = a.annotationType();
//获取注解的值,onClick注解上面的EventBase
EventBase base = annoType.getAnnotation(EventBase.class);
if (null == base) {
continue; //跳出本轮循环
}
/**
*拿到带注解的方法, 开始获取事件三要素 , 通过反射注入进去拿到真正的方法
*/
//1. 返回setOnclickListener字符串
String listenerSetter = base.listenerSetter();
//2. 返回View.OnClickListener字节码
Class<?> listenerType = base.listenerType();
//3. 返回onClick字符串
String callMethod = base.callBackMethod();
//保存方法名与方法的应映射, 在接下来的操作中方便使用
Map<String, Method> methodMap = new HashMap<>();
methodMap.put(callMethod, m);
try {
//拿到注解中的value方法
Method value = annoType.getDeclaredMethod("value");
//对应value方法的返回值,这里通过反射是为了通用性,如果指定具体的类可以直接获取,但是扩展性很低
int[] ids = (int[]) value.invoke(a);
//注入事件
for (int viewId : ids) {
Method findv = aClass.getMethod("findViewById", int.class);
//通过反射拿到View
View view = (View) findv.invoke(context, viewId);
if (null == view) continue;
/**
* @Param listenerSetter: setOnClickListener的字符串,可以反射出方法
* @Param listenerType: 参数类型,为View.OnClickListener.class
*/
Method setOnclick = view.getClass().getMethod(listenerSetter, listenerType);
ListenerInvactionHandler handler = new ListenerInvactionHandler(context, methodMap);
//设置返回对象的类型: proxyy是实现了OnclickListener接口,也就是listenerType接口的代理对象
Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(),
new Class[]{listenerType},
handler);
//利用代理设置监听
setOnclick.invoke(view,proxy);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
逻辑是复杂了一点 , 这里只是有一个三重的for循环 , 慢慢拆解下来 , 其实也不算很难 , 重要的注释都写在方法中了 , 去尝试一下吧 .
但是 , 懂的老铁们会说 , 你这个是运行时注解,在使用的时候,大量的反射会影响性能,是的,这个确实是一个很大的缺点,所以我们可以参考ButterKnife的做法,打造自己的编译时注解框架。