Dagger2 系列(一)Dagger2 入门简介

Dagger 是为 Java 和 Android 平台提供的一个完全静态的,在编译时进行依赖注入的框架。Dagger 由 Square 公司出品,Dagger2 是 Google 在 Dagger 基础上的二次开发。

Dagger2 官方资料

Github: https://github.com/google/dagger
官方文档: https://google.github.io/dagger/
API: https://google.github.io/dagger/api/latest/

Dagger2 优点

为什么要使用这个框架呢?

  • 提升开发效率,省去重复的简单体力劳动,同时使代码更加优雅。比如 new 一个实例的过程就是简单的重复的劳动,Dagger2 完全可以胜任此工作,使开发人员将精力放在关键业务上。
  • 更好的管理类实例。ApplicationComponent 管理整个 app 的全局类实例,所有的全局类实例都统一交给 ApplicationComponent 管理,并且它们的生命周期与 app 的生命周期一样。每个页面对应自己的 Component,页面 Component 管理着自己页面所依赖的所有类实例。因为 Component,Module,整个 app 的类实例结构变的很清晰。
  • 解耦。假如某类的构造函数发生变化,那些设计到的类都要进行修改,解耦避免了代码的僵化性。

依赖注入

Dagger2 是一个依赖注入框架,那么依赖注入又是什么呢?我们先把这个词拆开看,一个是依赖,一个是注入

class Player{
    MediaFile media;
    public Player() {
        media = new MediaFile();
    }
}

看上述代码,在 Player 类中,依赖一个 MediaFile 类的对象。依赖就是有联系,有地方使用到它就是对它有依赖,一个系统不可能完全的避免依赖。如果一个类在任何地方都没使用到,那么这个类就可以删掉了。注入就是将 MediaFile 的一个实例对象赋值给 media 引用。其实注入的方法有很多种,构造函数只是其中一种,我们还可以通过 setter 方法注入,如

public void setMedia(MediaFile media) {
        this.media = media;
}

通过 接口方式注入,如

class Player implements IPlayer {
    MediaFile media;
    public void play(IMediaFile file) {
        media = file;
    }
}

现在来看依赖注入,就是在有依赖需要的地方传入一个实例对象啊。那么依赖注入这个词到底是怎么来的呢?我们还是先看一下依赖倒置原则吧!

DIP,依赖倒置原则

DIP,英文全称 Dependence Inversion Principle,由软件开发大师 Robert C. Martin 提出,具体描述为:

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
  • 抽象不应该依赖于细节。细节应该依赖于抽象。

直接讲原则有点枯燥,假设我们想用播放器播放一段音频,看一段普通代码实现:

public class Test {
    Player player;
    MediaFile mediaFile;
    
    public void test(){
        player = new Player();
        mediaFile = new MediaFile();
        player.play(mediaFile); 
    }
}

上述代码就违反了 DIP 原则,Test 类 直接依赖低层模块 Player、MediaFile,而没有依赖于其抽象。我们可以用 DIP 原则修改,将 Player 抽象成 IPlayer 接口,MediaFile 抽象为 IMediaFile,让 DI 类去依赖二者的抽象,修改后代码如下所示,

public class DI {
    IMediaFile mediaFile;
    IPlayer player;
    public void test() {
        mediaFile = new MediaFile();
        player = new Player();
        player.play(mediaFile);
    }

    interface IPlayer {
        void play(IMediaFile file);
    }

    class Player implements IPlayer {
        public void play(IMediaFile file) {
        }
    }
    
    interface IMediaFile {
        String FilePath();

    }
    
    class MediaFile implements IMediaFile {
        public String FilePath() {
            return "";
        }
    }
}

注意:实际开发过程中不要为了实现某一原则而过度设计。

IOC ,控制反转

控制反转,Inversion of Control,是 Martin Flower 在2004 年总结提炼出来,也是面向对象重要的负责之一,旨在减小程序中不同部分的耦合问题。
在 DI 类中,我们只能生成固定的播放器和媒体文件,假如我想换一个播放器播放视频咋办,用哪个播放器播放音频或者视频,是否能够根据需求动态配置呢? IOC 就解决了这个问题。我们假设共有两款播放器:QQ 和 百度,两种媒体文件:音频和视频。我们可以把这种具体需求交给一个配置文件 config.properties(文件名、后缀名可随便写),然后让程序运行的时候去读取配置文件的内容去生成具体的实例。看代码(仅核心部分代码),

public static void main(String[] args) {
        // 采用哪种播放器,播放哪个媒体文件可以转给第三方框架控制,由第三方框架去读取配置文件(可行方式一),再决定生产哪个播放器实例和媒体文件的实例。
        // 控制权 由起先的用户 转给了第三方框架。
        Properties properties = new Properties();
        try {
            /**
             * 根据配置文件的位置进行读取数据,由于现在.properties文件在src目录下,所以直接
             * IoC.class.getClassLoader().getResourceAsStream("config.properties")就可以获取到配置文件的信息
             */
            properties.load(IoC.class.getClassLoader().getResourceAsStream("ioc/config.properties"));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        /**
         *将配置文件中的信息进行分割,返回成一个数组
         */

        String playerName = properties.getProperty("player");
        String mediaName = properties.getProperty("media");

        IPlayer _player = PlayerFactory.getPlayer(playerName);
        IMediaFile _mtype = MediaFactory.getMediaFile(mediaName);

        _player.play(_mtype);
    }

config.properties 文件内容如下,运行程序输出结果为“QQPlayer+video”。配置文件内容以键值对的形式存在,player 后对应是具体的播放器,media 后对应的是具体的媒体类型,这个可以按需修改。

player:qqq
media:video

IOC 和 DI 的关系:

所以控制反转 IoC(Inversion of Control)是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,比如转移交给了 IoC 容器,它就是一个专门用来创建对象的工厂,你要什么对象,它就给你什么对象,有了 IoC 容器,依赖关系就变了,原先的依赖关系就没了,它们都依赖 IoC 容器了,通过 IoC 容器来建立它们之间的关系。

DI(依赖注入)其实就是 IOC 的另外一种说法,DI是由Martin Fowler 在2004年初的一篇论文中首次提出的。他总结:控制的什么被反转了?就是:获得依赖对象的方式反转了。

简单使用

若想在工程中使用 Dagger2 框架,需要在 build.gradle 文件中添加配置:

dependencies {
    ...
    compile 'com.google.dagger:dagger:2.11-rc2'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.11-rc2'
}

若 Android Gradle plugin 插件版本低于 2.2,还需要引入 android-apt 插件,https://bitbucket.org/hvisser/android-apt

接下来就可以使用了,下文将会介绍最简单的用法并将Dagger 的整个框架串一下,高级用法及原理会在以后的文章中介绍。

case-1

假设我们有一辆下车,名字是兰博基尼,我们在 Activity 中将其名字显示出来。此用例只需用到两个注解,@Inject 和 @Component,@Inject 用在 Car 的构造函数上和被依赖的地方。@Component 相当于一个中间人的角色,负责牵线,将需要依赖的地方和提供依赖的地方连接起来。

public class Car {
    private String name = "我是一辆小车,Lamborghini";
    @Inject
    public Car() {
    }
    @Override
    public String toString(){
        return name;
    }
}

public interface CarComponent {
    void injects(MainActivity mainActivity);
}

Activity 代码如下

public class MainActivity extends AppCompatActivity {
    @Inject
    Car mCar;
    TextView mTv;

    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DaggerCarComponent.builder().build().injects(this);
        mTv = findViewById(R.id.tv);
        mTv.setText(mCar.toString());
    }
}

显示结果如下图,证明 mCar 经过了初始化,要不然会有空指针异常。


显示结果
case-2

case-1 有一个局限性,就是 @Inject 必须要修饰一个类的构造方法,若我们是应用的第三方工具包,我们是无法修改其源码的,这时候我们可以用 @Module 、@Provides来帮忙。

  • 第一步,用 @Module 声明一个类,代表这个类是拥有对外提供实例的功能。
  • 第二步,用 @Provides 修饰一个方法,该方法返回具体的实例对象。
  • 第三步,修改用 @Component 修饰的 interface,指定其需要的 Module 模块。
@Module
public class CarModule {
    @Provides
    public Car getsssssssCar(){
        return new Car();
    }
}

@Component(modules = CarModule.class)
public interface CarComponent {
    void injects(MainActivity mainActivity);
}

其余代码不变,运行程序,仍能看到上图显示结果。

相关注解

看上述代码,分析可以发现,要实现依赖注入至少需要三个角色:

  • 依赖需求者,类似于 MainActivity 中 用 @Inject 修饰的引用。
  • 依赖提供者,类似用 @Inject 修饰的构造函数和及 Module。
  • 中间人,将二者串联起来的角色,一个小手牵依赖的需求者,一个小手牵依赖的提供者。

我们将其中用到的注解解释下:

  • @Inject,注意下,这个注解类的全名称是javax.inject.Inject,是 Java 扩展包定义的注解。@Inject 可以修饰一个引用,代表此处可通过依赖注入传入一个实例对象,需要注意的是引用的访问修饰符不能是 private 和 protected,默认包访问权限即可;还可以修饰一个类的构造函数,作为一个标记,代表框架可能会根据该标调用此构造函数生成实例对象。
  • @Module、@Provides,主要是为了解决第三方类库问题而生的,Module 中可以定义多个创建实例的方法,这些方法用 @Provides 标注。
  • @Component,是一个中间人的角色(也可理解为干活的秘书),也是一个注入器,负责将生成的实例对象注入到依赖的需求者中,同时管理多个 Module。

我们将几个核心注解的功能以讲故事的方式在串一下,以小明要买玩具为例吧!

首先小明家境很富裕,他老爸直接给配了个秘书(@Component),小明有啥事都可以让秘书去做。小明看上了挖掘机的玩具(用 @Inject 修饰的引用),于是就叫秘书去买把。但是玩具哪有卖的呢?秘书说先去商店(@Module)逛一逛吧,假设该商店的一个货架(Provides)上正好有这个玩具,秘书直接买回去给小明就是了。若商店没有的话,商店老板告诉秘书我们这没有,我可以尝试联系一下玩具厂家(用@Inject 修饰的构造函数),看他们那有没有,他那有的话可以从那去取,没有的话就真的没有了。

依赖规则

再补充一下依赖关系,假如既用 @Inject 声明了构造函数,也在 Module 提供了响应方法,那么到底用哪个呢?Dagger2 的依赖规则是这样的:

  • 步骤1:查找Module中是否存在创建该类的方法。
  • 步骤2:若存在创建类方法,查看该方法是否存在参数
  • 步骤2.1:若存在参数,则按从步骤1开始依次初始化每个参数
  • 步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
  • 步骤3:若不存在创建类方法,则查找Inject注解的构造函数,看构造函数是否存在参数
  • 步骤3.1:若存在参数,则从步骤1开始依次初始化每个参数
  • 步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束

参考文献

https://www.jianshu.com/p/6eee326566b4
https://www.jianshu.com/p/cd2c1c9f68d4
https://blog.csdn.net/bestone0213/article/details/47424255