第一章 一堆废话
第01节 前置内容
最近在学习 Android 源码,在研究SystemUI源代码的过程中,发现了一段代码:
publicinterfaceStatusBarIconController...{}protectedStatusBarIconControllermIconController;protectedvoidgetDependencies(){mIconController=Dependency.get(StatusBarIconController.class);}// 后面发现存在大量的 Dependency.get(XXXX.class) 的操作!我当时特别好奇,这个Dependency.get(XXXX.class)在源代码当中,大量的出现,他是啥??
通过 AI 搜索,我发现了 他给我的解释是Dependency他是 Android 系统服务内部的一种轻量级依赖注入/服务定位器实现,用于解耦、简化构造、支持测试。
它虽然不如 Dagger 强大,但在 AOSP 这种对性能和启动速度敏感的场景下,足够高效且易于理解。
我发现这样的解释,说的有点不够清晰,不明白。
因为这里又有一些新的概念。依赖注入?
依赖注入(Dependency Injection,简称 DI)是一种软件设计模式,用于实现控制反转(Inversion of Control,IoC)。
它的核心思想是:对象的依赖(即它所需要的其他对象)不由自身创建或查找,而是由外部容器或框架在运行时“注入”进来。
简单来说:“我不要自己找我要的东西,你直接给我。”
哦,这里我看到了几个核心词汇DI和IOC
这两个词汇,我可以说并不陌生啊。
DI: 依赖注入,一种软件设计模式。 IOC: 了解过 Java Spring 框架的应该知道两大核心 IOC 和 AOP了解过IOC的同学,应该清楚知道。
在 Java 当中,一般的写法创建对象可以写为
Car mCar = new Car();这种是我们常规的写法,学过多态就知道,左右两侧是可以拆分的。
Car mCar1 = new CarBMW(); // 创建了一辆宝马汽车 Car mCar2 = new CarAudi(); // 创建了一辆奥迪汽车注意:等号左边可以看成是固定的,等号右边 由于 Java 多态的关系,我们可以看成不同的 Car 的创建方式。
如果将new的操作,我们自己最初的时候,并不去做,而是交给别人去做,别人做好了,创建好了,我只是拿过来使用 对象的内容!
这种不就是 IOC 的思想吗?
举个 生活中的例子
| 自己租房(传统方式) | 入住酒店(IOC) | |
|---|---|---|
| 控制权 | 我们需要自己找房源、签合同、交水电费、维修家具 | 我们只需要预定房间,酒店负责清洁、维修、换床单 |
| 依赖管理 | 我们需要自己管理水电、网络、家具等所有的依赖 | 酒店系统统一管理所有的服务 |
| 灵活性 | 我们可以随意的改造房间,但是承担所有责任 | 我们无法改变房间的布局,但是享受一站式服务 |
IOC的体现: 我们不再控制 “如何维护居住的环境”,而是将控制权交给酒店(容器)。我只需要提出需求(入住),酒店自动提供所有的配套服务。
当然,我这里不是去学习IOC的内容,只是去理解IOC的设计方式,整体来说:IOC就是一种DI依赖注入的方式。
那么和我们今天探讨的话题,Dagger有什么关系呢?
回到主题来说:
IOC控制反转,他是一种设计原则,而Dagger是一种具体的实现框架。
简单举例:
IOC 就像 "去餐厅吃饭" 这个概念。(我们不再需要自己做饭了) Dagger 就像是一家具体的餐厅, 比如 "海底捞" 它实现了 "去餐厅吃饭" 这个理念。对比一下Dagger和Spring IOC的区别
| 特性 | Dagger | Spring |
|---|---|---|
| 注入的时机 | 编译时(静态生成代码) | 运行时(反射) |
| 性能 | 高(无反射) | 较低(反射有开销) |
| 配置方式 | 注解 + 手动编写 Module | 注解 + XML / Java Config / 自动扫描 |
| 适用平台 | Android 为主,也可以用于 Java 企业级 | Java 后端 |
| 灵活性 | 较严格,需要显式声明所有依赖 | 更灵活,支持自动装载,懒加载等 |
通过上面的介绍,我们最终确定几点内容
1、Dagger 是一种 DI 依赖注入的方式 2、Dagger 主要应用在 Android 上 3、Dagger 性能高 4、Dagger 注入在编译时,会生成代码第02节 准备工作
Dagger我想要从一个最简单的JavaSE项目入手。
作为一个最简单的JavaSE项目,那么应该做哪些操作呢? (这里不存在 maven 依赖,有的仅有JavaSE代码 和lib jar包)
因为没有gradle和maven相关的内容,我全部采用的是lib jar包,所以前面需要去准备相关的jar包
相关 jar 包的下载地址:
maven 中央仓库地址 https://central.sonatype.com/
需要下载的依赖和版本:
| 编号 | 依赖名称 | 版本号 |
|---|---|---|
| 1 | annotation-api | 1.3.0 |
| 2 | auto-common | 0.1.0 |
| 3 | checker-qual | 2.5.2 |
| 4 | dagger | 2.24 |
| 5 | dagger-complier | 2.24 |
| 6 | dagger-complier-with-poet | |
| 7 | dagger-producers | 2.24 |
| 8 | dagger-spi | 2.24 |
| 9 | failureaccess | 1.0.1 |
| 10 | guava | 27.1 |
| 11 | javapoet | 1.11.0 |
| 12 | javax.inject | 1 |
为了方便大家快速学习,我已经将使用的 jar 包提供了下载地址。
点我进入jar包的下载地址
第03节 项目鸟瞰
在学习的过程中,在项目文档结构中,踩了不少的坑,防止后续有更多的人,踩坑,将项目的 鸟瞰图,拿出来对照,能够清晰的看明白代码结构。
配置文件studyDagger/studyDagger.iml内容如下:
<?xml version="1.0" encoding="UTF-8"?><moduletype="JAVA_MODULE"version="4"><componentname="NewModuleRootManager"LANGUAGE_LEVEL="JDK_17"inherit-compiler-output="true"><exclude-output/><contenturl="file://$MODULE_DIR$"><sourceFolderurl="file://$MODULE_DIR$/out/production/studyDagger/generated"isTestSource="false"generated="true"/><sourceFolderurl="file://$MODULE_DIR$/src"isTestSource="false"/></content><orderEntrytype="inheritedJdk"/><orderEntrytype="sourceFolder"forTests="false"/><orderEntrytype="library"name="lib"level="project"/></component></module>第二章 入门案例
第01节 案例代码
汽车工厂接口
packagecom.study;importdagger.Component;@ComponentpublicinterfaceCarFactory{CarBMWgetBMW();CarAudigetAudi();}宝马汽车类
packagecom.study;importjavax.inject.Inject;publicclassCarBMW{@InjectpublicCarBMW(){}publicStringgetName(){return"宝马汽车";}}奥迪汽车类
packagecom.study;importjavax.inject.Inject;publicclassCarAudi{@InjectpublicCarAudi(){}publicStringgetName(){return"奥迪汽车";}}测试类
packagecom.study;publicclassDemo{publicstaticvoidmain(String[]args){CarFactoryfactory=DaggerCarFactory.create();System.out.println("success: "+factory.getBMW().getName());// success: 宝马汽车System.out.println("success: "+factory.getAudi().getName());// success: 奥迪汽车}}第02节 流程图解
编译时的图解
运行时图解
第03节 总结思考
总结:
1、你看,在 Demo 的运行过程中, 并没有出现 new 的代码。 2、过程说明: A. Demo 想要车, 那么问 Factory 要! B. Factory 怎么知道 如何造车的呢? C. 我们在 Car 的构造方法上, 贴上了 @Inject 标签 D. 承上启下的纽带就是 Dagger 帮我们自动生成的 DaggerCarFactory 因此整个过程中, 我们并没有写一行的 new 代码, 但是车子却被 稳稳当当的造出来了!思考:
上面代码当中,仅仅是作为一个入门级的案例,目的是为了我们方便去理解代码运行过程。 如果,将它作为工业级项目,他存在一些致命的问题。 致命缺陷: 不符合"面向接口编程"原则 1、问题所在: CarFactory 直接返回了具体的实现类 CarBMW 和 CarAudi 如果未来有 100种车型, 工厂接口就要写 100个方法, 并且调用方 Demo 必须依赖这些具体的类, 违背了低耦合的设计原则。 2、拓展方向: 接口多实现与多态 A. 解决方案: 引入一个统一的 Car 接口, 让 CarBMW 和 CarAudi 去实现它 B. 在 Dagger 当中存在知识点: 学习使用 @Mudule 与 @Binds (或者 @Provides) 来告诉 Dagger 当别人管你要 Car 接口时, 我们应该实例化哪一个具体的实现类第三章 基础用法
第01节 案例代码
统一的接口 Car
packagecom.study;publicinterfaceCar{StringgetName();}基础子类 宝马汽车
packagecom.study;importjavax.inject.Inject;publicclassCarBMWimplementsCar{@InjectpublicCarBMW(){}publicStringgetName(){return"宝马汽车";}}基础子类 奥迪汽车
packagecom.study;importjavax.inject.Inject;publicclassCarAudiimplementsCar{@InjectpublicCarAudi(){}publicStringgetName(){return"奥迪汽车";}}模块管理类 CarModule
packagecom.study;importdagger.Binds;importdagger.Module;importjavax.inject.Qualifier;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;@ModulepublicabstractclassCarModule{@Qualifier@Retention(RetentionPolicy.RUNTIME)public@interfaceBMW{}@Qualifier@Retention(RetentionPolicy.RUNTIME)public@interfaceAudi{}@Binds@BMWpublicabstractCarbindBMW(CarBMWcarBMW);@Binds@AudipublicabstractCarbindAudi(CarAudicarAudi);}汽车工厂类
packagecom.study;importdagger.Component;@Component(modules=CarModule.class)publicinterfaceCarFactory{@CarModule.BMWCargetBMW();@CarModule.AudiCargetAudi();}测试类
packagecom.study;publicclassDemo{publicstaticvoidmain(String[]args){CarFactoryfactory=DaggerCarFactory.create();System.out.println("success: "+factory.getBMW().getName());// success: 宝马汽车System.out.println("success: "+factory.getAudi().getName());// success: 奥迪汽车}}第02节 总结思考
总结
上面的这套基础用法,是 Dagger 解决 "同一个接口, 有多个实现类" 的时候, 经典做法。 基于 @Qualifier 自定义注解的显示绑定优点
1、强制类型安全与编译期检查 每个类都有明确的自定义 注解(如 @BMW @Audi) 在 Component 接口也是显式声明的方法(如: getBMW) 如果配置错误, 编译期间, 直接报错, 绝对不会把奥迪错当成为宝马 2、直观易读 对于使用者来说, API 语义非常清晰 调用 factory.getBMW() 就可以明确知道拿到的是什么对象缺点 存在的痛点!
1、样板代码严重 每增加一个汽车品牌(如 奔驰 CarBenz) 我们必须成套的增加下面的内容: A. 一个新自定义的 @Qualifier 注解 B. 在 CarModule 当中添加一条 @Binds 抽象方法 C. 在 CarFactory 接口当中增加一个显式获取的方法 (如: getBenz()) 2、违反开闭原则(OCP) 核心工厂接口 CarFactory 被迫感知了所有的具体实现。 一旦需要支持新的车型,就必须修改 CarFactory 接口, 这种操作, 在大型项目或者插件化架构中是不可以接受的。 3、无法动态, 动态获取 所有的调用都是硬编码, 如果业务上需要根据用户输入的字符串 或者类名称 动态获取对应的汽车实例。 基础用法的代码, 很难优雅的做到, 通常需要写一堆的 if...else... 或者 switch....如何解决上述缺点?
为了解决上述的问题: 每增加一个类, 都需要改动多处接口, 以及 无法动态拓展的 痛点! 我们需要一种 能够自动手机实现类, 并且支持动态路由的机制。 这种方案正是 Dagger 的 Multibindings 多重绑定 派上用场的地方。第四章 多重绑定
第01节 案例代码
车辆品牌的接口
packagecom.study;publicinterfaceCarBrand{StringgetName();}车辆工厂接口
packagecom.study;importdagger.Component;importjava.util.Map;@Component(modules=CarModule.class)publicinterfaceCarFactory{Map<Class<?>,CarBrand>getCarMap();}基础子类 奥迪汽车
packagecom.study;importjavax.inject.Inject;publicclassCarAudiimplementsCarBrand{@InjectpublicCarAudi(){}@OverridepublicStringgetName(){return"奥迪汽车";}}基础子类,宝马汽车
packagecom.study;importjavax.inject.Inject;publicclassCarBMWimplementsCarBrand{@InjectpublicCarBMW(){}publicStringgetName(){return"宝马汽车";}}模块类
packagecom.study;importdagger.Binds;importdagger.Module;importdagger.multibindings.ClassKey;importdagger.multibindings.IntoMap;@ModulepublicabstractclassCarModule{@Binds@IntoMap@ClassKey(CarBMW.class)publicabstractCarBrandbindCarBrandBMW(CarBMWcarBMW);@Binds@IntoMap@ClassKey(CarAudi.class)publicabstractCarBrandbindCarBrandAudi(CarAudicarAudi);}测试类
packagecom.study;importjava.util.Map;publicclassDemo{publicstaticvoidmain(String[]args){CarFactoryfactory=DaggerCarFactory.create();Map<Class<?>,CarBrand>carMap=factory.getCarMap();CarBrandbrandAudi=carMap.get(CarAudi.class);CarBrandbrandBMW=carMap.get(CarBMW.class);System.out.println("success: "+brandAudi.getName());// success: 奥迪汽车System.out.println("success: "+brandBMW.getName());// success: 宝马汽车}}第02节 总结思考
总结
1、极佳的架构扩展性(符合开闭原则 OCP) A. Component 接口彻底解耦: CarFactory 只需要暴露一个统一的接口 getCarMap() B. 新增品牌零修改: 当需要增加奔驰(CarBenz)时,完全不需要修改 CarFactory 接口。 你只需要写好 CarBenz 类,并在 CarModule 中增加一个 @IntoMap 的绑定方法即可。 这极大地保护了核心架构的稳定性 2、支持动态路由与运行时查找 A. 在实际业务中,我们经常需要根据配置、数据库数据、或者用户的输入来决定实例化哪辆车。 B. 使用 Map 结构,你可以直接通过传递变量(如 Class<?>、或是将 Key 改为 String 后的字符串标识)来动态获取对应的实例。 这在第一套方案(每个品牌对应一个硬编码方法)中是无法直接做到的。 C. 告别自定义注解的样板代码 基础用法中,每加一个品牌都要手写一个 @Qualifier 注解(如 @BMW、@Audi)。 多重绑定方案直接利用了 Dagger 内置的 @ClassKey(或 @StringKey),省去了大量自定义注解的声明,代码结构更加干净整洁。思考 虽然多重绑定非常优雅,但在特定场景下它也有一些不可忽视的“隐患”:
1、丧失了编译期的“强类型安全”约束 A. 必须通过 carMap.get(CarAudi.class) 来获取实例。 B. 如果某个品牌忘记在 Module 中注册(或者拼写错了 Key),编译期不会报错,但在运行期 carMap.get(...) 会直接返回 null。 C. 如果你没有做非空校验(如 brand.getName()),就会触发 NullPointerException(空指针异常) 2、存在不必要的内存与性能开销(非懒加载问题) A. 即时初始化: 默认情况下,当你调用 factory.getCarMap() 时, Dagger 会一次性将 Map 里的所有汽车实例全部初始化(包括奥迪、宝马以及未来可能加入的几十个品牌) B. 性能隐患: 如果某些汽车类的初始化非常消耗资源(比如要连接数据库或加载大文件), 而你其实只需要使用其中一种,这种“全量初始化”就会造成严重的内存和性能浪费。 C. 解决方案: 在实际开发中,通常需要将 Map 的 Value 类型定义为双子(Provider)或懒加载(Lazy), 例如:Map<Class<?>, Provider<CarBrand>>,从而实现用哪个才初始化哪个。说到了懒加载的方式,我们回头看看,SystemUI的代码不就是 懒加载的吗!
前面我们说到了,多重绑定 和 懒加载 的实现方式,关于后续的思考,如何操作呢?
怎样代码,才能符合 商用级的标准? 怎样的代码才能符合 工业级标准?
想要了解更多,点击下面,进入我的下一篇文章吧。
点我,进入 依赖注入Dagger懒加载
第五章 注意事项
第01节 编译的依赖
因为在编译期间,需要找到相关的配置
主要位置: Settings ---> Build, Execution, Deployment ---> Compiler ---> Annoation Processors 1、注意需要配置 lib 文件夹下面的所有 jar 包 2、可选项: Use --processor--module-path compiler option (for Java 9 and later) 3、操作完毕之后, 点击 OK第02节 生成的out
这里生成的文件,会出现在out目录下面,需要对文件,作为Generated Sources Root
第03节 重新编译
每次修改完毕之后,建议重新编译一下代码,编译代码的方式是
在工具栏,找到 Build ---> Rebuild Project