xUtils源码分析 + 手写一个IOC注解框架
一、概念
说说IOC,控制反转(Inversion of Control)。概念的东西不做过多解释,比较典型的做法是使用了反射加注解。我们如果用过xUtils或者ButterKnife对这种形式应该不陌生,这些框架可以减少很多在开发过程当中的刻板代码的书写。而xUtils和ButterKnife的实现方式还不太一样,xUtils全面地使用了反射,而ButterKnife则主要使用了编译时注解轻量级反射。本文最后要全面使用反射搭建注解框架,所以下面对xUtils的源码稍作分析。下面是两个框架源码的地址:
xUtils : https://github.com/wyouflf/xUtils3
ButterKnife : https://github.com/JakeWharton/butterknife
二、xUtils 源码分析
xUtils 包含的内容比较多,有网络、数据库、IOC注入、网络图片使用等,这里主要讲讲IOC注入。
先看一下xUtils及基本使用:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 注入
x.view().inject(this);
}
// type 这个参数可选,默认是OnClickListener
@Event(R.id.main_layout, type = View.OnClickListener.class)
private void showToast(){
Toast.makeText(this, "xUtil test .", Toast.LENGTH_SHORT).show();
}
}
使用比较简单,来看看源码 inject() 这个方法是怎么注入的。找到这个类 :ViewInjectorImpl.java 。里面代码不多,就一百多行,主要的注入逻辑在这个方法里 :injectObject() 。现在挑主要部分看看:
@SuppressWarnings("ConstantConditions")
private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
..........
// 注释 1 view 的注入
Field[] fields = handlerType.getDeclaredFields();
if (fields != null && fields.length > 0) {
for (Field field : fields) {
Class<?> fieldType = field.getType();
......
// 注解 ViewInject.class 注释 3
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
try {
View view = finder.findViewById(viewInject.value(), viewInject.parentId());
if (view != null) {
field.setAccessible(true);
field.set(handler, view);
} else {
throw new RuntimeException("Invalid @ViewInject for "
+ handlerType.getSimpleName() + "." + field.getName());
}
} catch (Throwable ex) {
LogUtil.e(ex.getMessage(), ex);
}
}
}
} // end inject view
// 注释 2 event 事件注入
Method[] methods = handlerType.getDeclaredMethods();
if (methods != null && methods.length > 0) {
for (Method method : methods) {
//// 注解 Event.class 注释 4
Event event = method.getAnnotation(Event.class);
if (event != null) {
try {
// id参数
int[] values = event.value();
int[] parentIds = event.parentId();
int parentIdsLen = parentIds == null ? 0 : parentIds.length;
//循环所有id,生成ViewInfo并添加代理反射
for (int i = 0; i < values.length; i++) {
int value = values[i];
if (value > 0) {
ViewInfo info = new ViewInfo();
info.value = value;
info.parentId = parentIdsLen > i ? parentIds[i] : 0;
method.setAccessible(true);
// 对注解方法进行事件添加
EventListenerManager.addEventMethod(finder, info, event, handler, method);
}
}
} catch (Throwable ex) {
LogUtil.e(ex.getMessage(), ex);
}
}
}
} // end inject event
}
删除了一小部分。这就清晰明了了。用反射获取了对象里的所有属性以及所有的方法(包括私有的)。然后就是两个 for 循环,分别遍历了对象里的所有属性和所有方法。分别找到被ViewInject.class注解标记的属性以及被Event.class注解标记的方法(注释3、注释4)。然后分别对属性进行 View的注入(field.set(handler, view);)、对方法进行事件添加。可以看到,是框架为我们写了 findViewById,以及用反射为我们调用了方法。
三、手写一个简单IOC注解框架
下面仿照 xUtils手写一个IOC注解框架。主要实现 View的注入及事件的注入,除了点击事件外,还有一个网络检测的注解。
1.自定义注解
先自定义三个注解,分别标记 View的注入、点击事件及网络检查。
FindView.java
/******************************************************************/
/**
* View 注入
*/
// RUNTIME-运行时,CLASS-编译时,SOURCE-元注解
@Retention(RetentionPolicy.RUNTIME)
//作用于 属性:FIELD , 方法:METHOD , 类:TYPE , 参数:PARAMETER
// 构造器:CONSTRUCTOR , 注解:ANNOTATION_TYPE
@Target(ElementType.FIELD)
public @interface FindView {
// 穿 View的 id
int value();
}
OnClick .java
/*********************************************************************/
/**
* 点击事件
*/
// 运行时
@Retention(RetentionPolicy.RUNTIME)
// 方法
@Target(ElementType.METHOD)
public @interface OnClick {
int value();
}
CheckNet .java
/****************************************************************/
/**
* 网络检查
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckNet {
}
上面就是自定义框架使用到的三个注解,有注释。注解的定义就不细说了。
2.实现注入
看代码:
// CleanBinder.java
/**
* 绑定 View
*
* @param object
*/
private static void bindView(ViewFinder viewFinder, Object object){
if (object == null) return;
Class<?> clazz = object.getClass();
// 反射获取对象里所有属性
Field [] fields = clazz.getDeclaredFields();
// 遍历所有属性,找到添加了 FindView注解的,然后绑定
for (Field field : fields){
FindView findView = field.getAnnotation(FindView.class);
if (findView != null){
// 1 拿到注解里声明的 id
int viewId = findView.value();
// 2 根据 id 获取到 View
View view = viewFinder.findViewById(viewId);
// 设置属性可访问,包括私有的
field.setAccessible(true);
// 3 再将 View与属性绑定
try {
field.set(object, view);
} catch (IllegalAccessException e) {
Log.d("CleanBinder", "IllegalAccessException - " + field.getName().toString());
e.printStackTrace();
}
}
}
}
/**
* 绑定点击事件注解
*
* @param object
*/
private static void bindMethod(ViewFinder viewFinder, Object object){
if (object == null) return;
Class<?> clazz = object.getClass();
// 获取所有方法
Method[] methods = clazz.getDeclaredMethods();
// 遍历所有方法 找到有注解的
for (Method method : methods){
OnClick onClick = method.getAnnotation(OnClick.class);
if (onClick == null) continue;
// 1 拿注解里声明的 ID
int viewID = onClick.value();
// 2 再拿 View
View view = viewFinder.findViewById(viewID);
// 3 监听
view.setOnClickListener((View view1) -> {
CheckNet checkNet = method.getAnnotation(CheckNet.class);
if ((checkNet != null) && (!isNetAvailable((Context) object))){
Toast.makeText((Context) object, "请检查网络连接", Toast.LENGTH_SHORT).show();
return;
}
// 访问权
method.setAccessible(true);
try {
// 调用点击事件绑定的方法
method.invoke(object);
} catch (IllegalAccessException e) {
Log.d("CleanBinder", "IllegalAccessException - " + method.getName());
e.printStackTrace();
} catch (InvocationTargetException e) {
Log.d("CleanBinder", "InvocationTargetException - " + method.getName());
e.printStackTrace();
}
});
}
}
跟Utils差不多, 都是使用了反射遍历所有属性和方法。然后反射注入,就是上面用了两个方法。两个方法用来分别执行 View的注入及事件注入,其中第二个事件注入的方法实现了点击事件和网络检查。当点击事件来时,先进行网络检查。没有网络时就弹土司提示,而不再执行反射调用方法。
3.框架的使用
使用上肯定也要便捷:
public class MainActivity extends AppCompatActivity {
@FindView(R.id.clean_id)
private Button mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CleanBinder.bind(this);
mTextView.setText("碧云天");
}
@OnClick(R.id.clean_id)
@CheckNet
private void showToast(){
Toast.makeText(this, "碧云天", Toast.LENGTH_SHORT).show();
}
}
Screenrecorder-2021-07-10-19-17-18-33120217102225241.gif
上面是使用方式和效果图。可以看到,点击事件和网络检查就分别使用了一个注解就实现,这种方式的意义就在于减少刻板代码量。断网后点击按钮时会在框架里弹出“请检查网络连接”的提示。整块功能主要部分就是上面两个方法。一共有5个类,其中3个是注解,有一个是工具类。源码:Github源码