Android进阶篇- IOC注入架构

前言

在平时写代码的过程中都会出现很多方法中出现@Override @hide这样的注解,还有在比如我们经常用到的EventBus、ButterKnife、Retrofit、Dagger等都会用到。它们有什么作用?以及怎么使用的?改文章也会对上一篇反射的技术进行进一步的加深使用和理解。如果没有使用反射技术童鞋请先阅读上一篇 [Android进阶篇-反射机制ReFlect]

IOC

  • 初始 DIP、Ioc、DI、Ioc容器

依赖倒置原则 (DIP, Dependency Inverse Principle)
强调系统的“高层组件”不应当依赖于“底层组件”, 并且不论是“高层组件”还是“底层组件”都应当依赖于抽象。抽象不应当依赖于实现,实现应该依赖于抽象(软件设计原则)

控制反转(Ioc,Inverse of Control)
一种反转、依赖和接口的方式。就是将控制权“往高处/上层”转移,控制反转是实现依赖倒置的一种方法(DIP的具体实现方式)

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

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

  • 作用以及优缺点

Ioc的核心是解耦,简化我们的工作量。
而解耦的目的:修改耦合对象时不影响另外一个对象,降低模块之间的关联。
在Spring中IOC更多是依靠xml的配置,而在Android中的IOC框架可以不使用xml配置。

优点:代码量减少,代码阅读性好
缺点:会产生一定的性能消耗 (现今的手机上是可以忽略的)

  • 元注解

定义定义注解时,会需要一些元注解(meta-annotation),如@Target@Retention@Target用来定义你的注解将应用于什么地方(可能是一个类或者是一个方法),@Retention用来定义注解在哪一个级别可用(是在源码中SOURCE,还是在类文件中CLASS,又或者是在运行时RUNTIME)
定义注解时很像是在定义一个接口,所以一般在注解中都会包含一些元素以表示某些值,当处理注解时,就可以根据这个值来做一些判断。

@Target
Type:类/接口
FIELD:属性
METHOD:方法
PARAMETER:参数
CONSTRUCTOR:构造方法
LOCAL_VARIABLE:局部变量
ANNOTATION_TYPE:该注解使用在另一个注解上
PACKAGE:包

@Retention
注解会在class字节码文件中存在, JWM加载时可以通过反射获取到该注解的内容
SOURCE:源码级操作(检查、检测)
CLASS:在编译时 进行一些预操作
RUNTIME:运行时编译
生命周期:SOURCE < CLASS < RUNTIME

  1. 一般如果需要在运行时去动态获取注解信息,用RUNTIME注解
  2. 要在编译时进行一些预处理操作,如ButterKnife,用CLASS注解。注解会在class文件中存在,但是在运行时会被丢弃
  3. 做一些检查性的操作,如@Override,用SOURCE源码注解。注解仅存在源码级别,在编译的时候丢弃该注解
  • 注解元素

在定义注解时,我们同样可以为注解定义元素,例如:

@Target(ElementType.TYPE) //作用在类之上
@Retention(RetentionPolicy.RUNTIME) //运行时编译
public @interface User {
      int age(); //返回int类型数据
      String name() default "daxu"; //返回String类型数据
}

标签@User是由User.class定义,其中包含了int元素的age,以及一个String元素的name,如果name不传值则默认取值为“daxu”。在注解元素中,可用的类型如下:

所有的基本类型(int, float, boolean 等)
String
Class
enum
Annotaion
以上类型的数组

如果使用了其他类型则会编译报错。注意,也不允许使用任何包装类型。

  • 使用

在Andorid开发中我们经常在Activity的onCraete中写setContentView(R.layout.xxx),最烦的是每个view的申明都需要些TextView tv = findviewById(R.id.tv), 所以有大神推出了ButterKnife使用注解来解决。今天我们就来自己手动写一下。

打开AS创建一个工程,并且创建一个Java的Library,在该lib中创建一个Kind为Annotation,Name为ContentView的注解类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
    int value();
}

使用该注解时在Activity之上申明该注解,并传入该Activity的layout

@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {
}

到此,注解已经定义完成。那么怎么去完成把layout和Activity进行绑定呢?下面我们在lib中创建一个InjectManager的类。

public class InjectManager {
  public static void inject(Activity activity) {
    // 布局的注入
    injectLayout(activity);
  }
  
  private static void injectLayout(Activity activity) {
    //根据传递的Activity对象获取类
    Class clazz = activity.getClass();
    //获取类之上的注解
    ContentView contentView = clazz.getAnnotation(ContentView.class);
    if (contentView != null) {
      //获取注解的值,也就是传递的R.layout.activity_main
      int layoutId = contentView.value();
      //第一种方式 (这种方式比较low)
      //activity. setContentView(layoutId);
      //第二种方式:反射方法
      try {
          Method method = clazz.getMethod("setContentView", int.class);
          //执行方法
          method.invoke(activity, layoutId);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

并且在BaseActivity的onCreate()方法中进行注册

public class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //注册
        InjectManager.inject(this);
    }
}

开始run一下项目,发现该MainActivity正常打开。
按照此种方式我们继续完成View的申明。比较findviewbyid才是最让人写吐的...
在lib中创建一个Kind为Annotation,Name为BindView的注解类

@Target(ElementType.FIELD) //作用在属性之上
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}

@BindView和@ContentView基本一致,其差别就是这次作用在属性之上,而ContentView则作用在类之上。
定义好注解后,我们继续在InjectManager中开始编写代码, 在InjectManager中定义一个方法injectView(Activity activity); 在inject()进行调用

private static void injectView(Activity activity) {
  //获取类
  Class clazz = activity.getClass();
  //拿到每个属性 (因为不确定是什么修饰 所以使用getDeclaredFields)
  Field[] fields = clazz.getDeclaredFields();
  for (Field field : fields) {
    //获取每个属性上的注解
    BindView bindView = field.getAnnotation(BindView.class);
     if (bindView != null) {
      //拿到控件ID
      int viewId = bindView.value();
      try {
          Method method = clazz.getMethod("findViewById", int.class);
          Object view = method.invoke(activity, viewId);
          //设置private的访问权限
          field.setAccessible(true);
          //将方法执行的返回值赋值给全局的某属性
          field.set(activity, view);
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
  }
}

完成后在MainActivity中测试.. 为了测试view被正常申明,我们在onResume()方法中弹出吐司,吐司内容为该Button按钮的内容来测试一下。

@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {
    @BindView(R.id.btn)
    public Button btn;

    @Override
    protected void onResume() {
        super.onResume();
        Toast.makeText(this, btn.getText().toString(), Toast.LENGTH_LONG).show();
    }
}

run项目发现项目运行正常, 并且成功弹出Button内容的吐司~


Android进阶篇- IOC注入架构_第1张图片
image.png

目前呢, MainActivity中的setContentView和findViewById已经使用了注解的方式进行了简化,还能有其他的地方可以使用注解来进行快速开发吗?答案是当然有!在Android开发中还有很多地方都可以,比如:OnClickListener,onLongClickListener等点击事件,像这种点击事件它又和setContentView和findViewById完全不一样,因为这种事件都带有各自回调方法.. 而我们所有的操作也都写在了onClick的回调方法里面 这种情况下要怎么处理呢?

btn.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
  }
});

btn.setOnLongClickListener(new View.OnLongClickListener() {
  @Override
  public boolean onLongClick(View v) {
     return false;
  }
 });

看到上面的两个点击事件,我们发现了三个共同点(事件三部曲)
① 都有监听的方法名 setxxxListener
② 都有监听的对象 new View.OnxxxListener
③ 都有回调方法名 onxxx(View v)
找到了规律,我们是否可以想一个办法,把这三个规律整合在一起并且“委托一个人”帮忙把回调里的事情给办了呢? 委托这两个字在我们开发人员眼中很容易就会想到一个词“代理”。代理模式不就是这样的吗? 简单点来讲,就是能不能把事件三部曲打包成一个对象,用代理去完成这件事..
现在点击事件的规律找到了,怎么去委托也知道了,但是怎么让代理完成的事情和我们定义的方法绑定到一起呢? 这时就得需要用到另一个知识点 AOP面向切面编程。

AOP面向切面的知识点下次再来讲吧..

你可能感兴趣的:(Android进阶篇- IOC注入架构)