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<? extends Activity> 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<? extends Activity> 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内容的吐司~


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面向切面的知识点下次再来讲吧..

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容

  • 2.1 我们的理念是:让别人为你服务 IoC是随着近年来轻量级容器(Lightweight Container)的...
    好好学习Sun阅读 2,709评论 0 11
  • 本来是准备看一看Spring源码的。然后在知乎上看到来一个帖子,说有一群**自己连Spring官方文档都没有完全读...
    此鱼不得水阅读 6,932评论 4 21
  • 原文地址,此处只为学习先来讲一讲,一个简单的依赖注入例子。 依赖 如果在 Class A 中,有 Class B ...
    lltree阅读 1,829评论 2 8
  • 怎么让人发笑? 让他意外/让他优越/让他宣泄,笑的原因千差万别,但都基于这三种原理。 呀,好像幽默有章可循,这真是...
    大嘴8阅读 286评论 0 1
  • 在计算机中是采用二进制,这样造成在操作系统中对容量的计算是以每1024为一进制的,每1024字节为1KB,每102...
    HelloWorld_26阅读 217评论 0 0