【公开课实践】之IoC注入
2019-04-27 本文已影响17人
哈利迪ei
概念
- 依赖倒置原则(DIP,Dependency Inverse Principle)
抽象不依赖于实现,实现依赖于抽象。
- 控制反转(IoC,Inverse of Control)
将控制权往上层转移,是实现依赖倒置的一种方法。
- 依赖注入(DI,Dependency Injection)
组件通过构造函数或setter方法,将其依赖暴露给上层,上层要设法取得组件的依赖,并将其传递给组件。依赖注入是实现控制反转的一种手段。
- IoC容器
依赖注入的框架,用来映射依赖,管理对象创建和生存周期。(DI框架)
解决问题
- 控件初始化。如一个页面控件很多,就会写上一堆诸如
Button btn = findViewById(R.id.btn)
这样的初始化代码,这些重复性的代码显得很冗余,像下面这样用注解标记来注入就行了。
@InjectView(R.id.btn)
Button btn;
- 控件多的同时也会有很多点击事件,如
setOnClink()
这样的方法来给控件设置点击事件,这些代码也可以用注解来实现:
@OnClick({R.id.btn})
public void clickEvent(View v){
//事件处理
}
代码实践
课中采用的方案主要还是运行期注解+反射
,不像ButterKnife
那样用APT在编译期生成class文件,这里先撇开反射的性能问题,主要学习设计思路。
首先新建BaseActivity
,onCreate
方法里开启注入:
InjectManager.inject(this);
然后看到注入管理器InjectManager
,其分了3个注入步骤:
public static void inject(Activity activity) {
//布局注入
injectLayout(activity);
//控件注入
injectView(activity);
//事件注入
injectEvent(activity);
}
- 首先是布局注入方法:
private static void injectLayout(Activity activity) {
//获取类
Class<? extends Activity> clazz = activity.getClass();
//获取类的注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null) {
//获取布局id
int layoutId = contentView.value();
//可以直接set
//activity.setContentView(layoutId);
try {
Method method = clazz.getMethod("setContentView", int.class);
method.invoke(activity, layoutId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// -------------------------------------- 相关注解
@Target(ElementType.TYPE) //作用在类上
@Retention(RetentionPolicy.RUNTIME) //存活到运行期
public @interface ContentView {
int value();
}
该方法通过反射来调用setContentView
,看看MainActivity
:
//加上注解,注入管理器通过该注解取到布局id,反射调用
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {
}
- 然后是控件注入方法:
private static void injectView(Activity activity) {
//获取类
Class<? extends Activity> clazz = activity.getClass();
//获取类的所有属性
Field[] fields = clazz.getDeclaredFields();
try {
//遍历属性
for (Field field : fields) {
//获取属性上的注解
InjectView injectView = field.getAnnotation(InjectView.class);
if (injectView != null) {
//获取id
int viewId = injectView.value();
//可以直接find
//Object view = activity.findViewById(viewId);
Method method = clazz.getMethod("findViewById", int.class);
Object view = method.invoke(activity, viewId);
field.setAccessible(true);
//给属性赋值
field.set(activity, view);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// -------------------------------------- 相关注解
@Target(ElementType.FIELD) //作用在成员变量上
@Retention(RetentionPolicy.RUNTIME) //存活到运行期
public @interface InjectView {
int value();
}
也是通过注解+反射来实现,看回MainActivity
:
public class MainActivity extends BaseActivity {
@InjectView(R.id.tv)
private TextView mTv;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//无需手动find,直接使用
mTv.setText("hello ioc");
}
}
- 最后来看事件注入方法:
private static void injectEvent(Activity activity) {
//获取类
Class<? extends Activity> clazz = activity.getClass();
//获取所有方法
Method[] methods = clazz.getDeclaredMethods();
try {
//遍历
for (Method method : methods) {
//获取每个方法的注解,可能有多个注解
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
//获取注解类型
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType != null) {
EventBase eventBase = annotationType.getAnnotation(EventBase.class);
if (eventBase != null) {
//获取3个成员
String listenerSetter = eventBase.listenerSetter();
Class<?> listenerType = eventBase.listenerType();
String callbackListener = eventBase.callbackListener();
Method valueMethod = annotationType.getDeclaredMethod("value");
//获取id数组
int[] viewIds = (int[]) valueMethod.invoke(annotation);
//拦截方法,执行自定义方法
ListenerInvocationHandler handler = new ListenerInvocationHandler(activity);
handler.addMethod(callbackListener, method);
//代理
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, handler);
for (int viewId : viewIds) {
View view = activity.findViewById(viewId);
Method setter = view.getClass().getMethod(listenerSetter, listenerType);
setter.invoke(view, listener);
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// -------------------------------------- 相关注解
//元注解
@Target(ElementType.ANNOTATION_TYPE) //作用在注解上
@Retention(RetentionPolicy.RUNTIME) //存活到运行期
public @interface EventBase {
//事件有3个成员
//1. 方法名,如"setOnClickListener"
String listenerSetter();
//2. 监听类型,如View.OnClickListener.class
Class<?> listenerType();
//3. 回调方法,如"onClick"
String callbackListener();
}
@Target(ElementType.METHOD) //作用在方法上
@Retention(RetentionPolicy.RUNTIME) //存活到运行期
@EventBase(listenerSetter = "setOnClickListener", listenerType = View.OnClickListener.class, callbackListener = "onClick")
public @interface OnClick {
int[] value();
}
代码较多,首先,EventBase
作为元注解,对常规点击事件设置过程进行抽象,例如:
OnClick
注解则作为具体注解,将具体值传给元注解。然后注入过程是:
- 获取Activity方法进行遍历
- 获取方法上的注解进行遍历
- 获取注解类型
- 通过注解类型获取元注解
EventBase
- 获取
EventBase
3个成员,备用 - 注解类型获取方法
value
,value
执行获取控件id数组 - 创建拦截器,添加需要拦截的方法和用户自定义方法
- 创建代理
- 遍历控件id数组,通过代理为每个控件设置点击事件
关于代理、拦截器可以看看这篇文章:代理的概念
拦截处理类ListenerInvocationHandler
如下:
public class ListenerInvocationHandler implements InvocationHandler {
//需要进行方法拦截的act
private Object mTarget;
//拦截的键值对
private HashMap<String, Method> mMethodMap = new HashMap<>();
public ListenerInvocationHandler(Object target) {
mTarget = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method != null) {
String methodName = method.getName();
Log.d("千然", "目标方法:" + methodName);
method = mMethodMap.get(methodName);
Log.d("千然", "替换方法:" + method.getName());
if (method != null) {
return method.invoke(mTarget, args);
}
}
return null;
}
/**
* 添加需要拦截的方法
*
* @param methodName 本应该执行的方法,如"onClick"
* @param method 执行自定义的方法,即含有注解的方法
*/
public void addMethod(String methodName, Method method) {
mMethodMap.put(methodName, method);
}
}
回到MainActivity
:
@InjectView(R.id.btn)
private Button mBtn;
@OnClick({R.id.btn})
public void clickEvent(View view) {
//点击按钮,修改文本
mTv.setText("change text by btn");
}
完整工程见github