注解

【公开课实践】之IoC注入

2019-04-27  本文已影响17人  哈利迪ei

概念

抽象不依赖于实现,实现依赖于抽象。

将控制权往上层转移,是实现依赖倒置的一种方法。

组件通过构造函数或setter方法,将其依赖暴露给上层,上层要设法取得组件的依赖,并将其传递给组件。依赖注入是实现控制反转的一种手段。

依赖注入的框架,用来映射依赖,管理对象创建和生存周期。(DI框架)

解决问题

  1. 控件初始化。如一个页面控件很多,就会写上一堆诸如Button btn = findViewById(R.id.btn)这样的初始化代码,这些重复性的代码显得很冗余,像下面这样用注解标记来注入就行了。
@InjectView(R.id.btn)
Button btn;
  1. 控件多的同时也会有很多点击事件,如setOnClink()这样的方法来给控件设置点击事件,这些代码也可以用注解来实现:
@OnClick({R.id.btn})
public void clickEvent(View v){
     //事件处理
}

代码实践

课中采用的方案主要还是运行期注解+反射,不像ButterKnife那样用APT在编译期生成class文件,这里先撇开反射的性能问题,主要学习设计思路。
首先新建BaseActivityonCreate方法里开启注入:

InjectManager.inject(this);

然后看到注入管理器InjectManager,其分了3个注入步骤:

public static void inject(Activity activity) {
    //布局注入
    injectLayout(activity);
    //控件注入
    injectView(activity);
    //事件注入
    injectEvent(activity);
}
  1. 首先是布局注入方法:
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 {
}
  1. 然后是控件注入方法:
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");
    }
}
  1. 最后来看事件注入方法:
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注解则作为具体注解,将具体值传给元注解。然后注入过程是:

  1. 获取Activity方法进行遍历
  2. 获取方法上的注解进行遍历
  3. 获取注解类型
  4. 通过注解类型获取元注解EventBase
  5. 获取EventBase3个成员,备用
  6. 注解类型获取方法valuevalue执行获取控件id数组
  7. 创建拦截器,添加需要拦截的方法和用户自定义方法
  8. 创建代理
  9. 遍历控件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

上一篇下一篇

猜你喜欢

热点阅读