Android |《看完不忘系列》之dagger
嗨,我是哈利迪~《看完不忘系列》将以从树干到细枝
的思路分析一些技术框架,本文将对开源项目dagger
进行介绍。
本文约3800字,阅读大约10分钟。
Dagger源码基于最新版本2.28.3
背景
依赖注入(Dependency Injection,DI)遵循控制反转(Inversion of Control,IoC)原则,简单来说就是创建对象时给对象传入依赖
,通过传入不同实例来实现不同行为(控制),比如常见的构造方法和setter都叫注入。
简单概括一下谷歌的造车栗子,
一、不注入,由Car类自己创建依赖的Engine实例,当需要替换汽车引擎时,需要修改Car类,违背了开放封闭原则,
class Car { private Engine engine = new Engine(); }
二、手动依赖注入,如构造方法和setter,当需要替换汽车引擎时,传入不同的引擎实现(如ElectricEngine extends Engine)即可,无需修改Car类,
//1.构造方法注入 class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } } //2.setter注入 class Car { private Engine engine; public void setEngine(Engine engine) { this.engine = engine; } }
三、随着汽车配件变多,依赖注入的代码也会越来越多,如汽车需要引擎、轮胎,引擎又需要气缸和火花塞,这样一层层下去注入代码成指数级上升,出现了大量创建对象的样板代码,
class Car { private final Engine engine;//引擎 private final Wheel wheel;//轮胎 public Car(Engine engine,Wheel wheel) { this.engine = engine; this.wheel = wheel; } } class Engine { private final Cylinder cylinder;//气缸 private final SparkPlug sparkPlug;//火花塞 public Car(Cylinder cylinder,SparkPlug sparkPlug) { this.cylinder = cylinder; this.sparkPlug = sparkPlug; } } void main(){ //为了造辆车,要new出一堆对象,还要构造方法注入,套娃既视感 Cylinder cylinder = new Cylinder(); SparkPlug sparkPlug = new SparkPlug(); Engine engine = new Engine(cylinder,sparkPlug); Wheel wheel = new Wheel(); Car car = new Car(engine,wheel); }
从代码可以看出有两大痛点,一是要创建很多对象
,二是对象的创建过程还需有序
,即要先有气缸和火花塞才能造引擎,先有引擎和轮胎才能造车,这样使用方还需花时间了解配件间的依赖关系
,很是不便。
于是就有了一些库来实现自动依赖注入,有两个实现思路(koin的实现以后再聊~),
- 一是运行期反射连接依赖项,编译影响小,但运行慢
- 二是编译期就连接依赖项,创建辅助类需要额外的io和编译耗时,会拖慢编译速度,但运行快
像Android内存和算力都有限的终端设备,dagger当然是选择思路2啦。dagger通过注解
标记对象创建姿势
、依赖关系
、作用域
等信息,在编译期创建辅助类,从而实现自动依赖注入。不过dagger的上手成本略高,谷歌后来又推出了Hilt,旨在让我们用得舒心,
Hilt 是推荐用于在 Android 中实现依赖项注入的 Jetpack 库。Hilt 通过为项目中的每个 Android 类提供容器并自动为您管理其生命周期,定义了一种在应用中执行 DI 的标准方法。
Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 提供的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。
-- 谷歌
Hilt就先放一放,下面我们先开始dagger之旅吧~
补:关于手动注入的痛点,可以看下谷歌的手动依赖项注入(看完或许能更好的理解dagger的设计)。
树干
简单使用
依赖,
implementation 'com.google.dagger:dagger:2.28.3' annotationProcessor 'com.google.dagger:dagger-compiler:2.28.3'
@Inject和@Component
@Inject标记实例的创建姿势,汽车和引擎类,
class Car { private final Engine mEngine; @Inject //告诉dagger如何创建Car public Car(Engine engine) { mEngine = engine; } public void start() { mEngine.start(); } } class Engine { @Inject //告诉dagger如何创建Engine public Engine() { } public void start() { Log.e("哈利迪", "引擎发动,前往秋名山"); } }
这样dagger就能生成类似 new Car(new Engine()) 的代码来创建实例,
@Component标记所要创建的实例有哪些,如在造车图纸(接口)里声明要造车,
@Component //告诉dagger要造啥 interface CarGraph { //造车 Car makeCar(); }
make一下项目,生成Car_Factory、Engine_Factory和DaggerCarGraph三个类,在Activity中使用,
class DaggerActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_dagger); //得到造车图纸 CarGraph carGraph = DaggerCarGraph.create(); //造出一辆汽车 Car car = carGraph.makeCar(); //引擎发动 car.start(); } }
注入姿势二,还可以直接把汽车注入给Activity,
@Component //告诉dagger要造啥 public interface CarGraph { //造车 Car makeCar(); //新增:告诉dagger有个Activity要注入 void inject(DaggerActivity activity); }
make一下,这时会多出一个类DaggerActivity_MembersInjector(成员注入器),我们后面再看,在Activity中,
class DaggerActivity extends AppCompatActivity { //向Activity注入汽车 @Inject Car mCar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_dagger); //得到造车图纸 CarGraph carGraph = DaggerCarGraph.create(); //告诉dagger有个Activity要注入 carGraph.inject(this); //此时,mCar已被dagger自动赋值,我们可以直接发动引擎 mCar.start(); } }
不管是哪种姿势,都可以看出,Activity只管提车,无需关心造车内部的细节(用了什么引擎),这样以后要换引擎了,Activity和Car也不用改动代码。
@Module和@Binds
那么问题来了,通常我们都是面向接口编程,现在想把Engine换成IEngine接口咋整呢?因为我有两种引擎,分别是汽油车的GasEngine,和电动车的ElectricEngine,接口没有构造方法怎么注入?此时@Module和@Binds注解就派上用场了。
定义引擎接口和实现类,
interface IEngine { public void start(); } class GasEngine implements IEngine {//汽油引擎 @Inject public GasEngine() { } @Override public void start() { Log.e("哈利迪", "汽油 引擎发动,前往秋名山"); } } class ElectricEngine implements IEngine {//电动引擎 @Inject public ElectricEngine() { } @Override public void start() { Log.e("哈利迪", "电动 引擎发动,前往秋名山"); } }
然后Car我们不要了,新写一个汽车类NewCar,让他依赖于接口IEngine而非类,
class NewCar { //依赖于接口 private final IEngine mEngine; @Inject //告诉dagger如何创建NewCar public NewCar(IEngine engine) { mEngine = engine; } public void start() { mEngine.start(); } }
下面有请@Module和@Binds登场!建一个抽象类GasEngineModule,表示汽油引擎模块,用于提供汽油引擎,
@Module abstract class GasEngineModule {//汽油引擎模块 @Binds abstract IEngine makeGasEngine(GasEngine engine); }
然后在造车图纸CarGraph里支持一下NewCar的创建,把Module引入Component,
@Component(modules = {GasEngineModule.class}) //引入汽油引擎模块 public interface CarGraph { //... //造新车 NewCar makeNewCar(); }
Activity使用,
//DaggerActivity.java void onCreate(Bundle savedInstanceState) { CarGraph carGraph = DaggerCarGraph.create(); NewCar newCar = carGraph.makeNewCar(); newCar.start(); }
搞定~
这时如果想要把NewCar换成电动引擎,直接依赖新的模块即可,新增模块ElectricEngineModule,用于提供电动引擎,
@Module public abstract class ElectricEngineModule {//电动引擎模块 @Binds abstract IEngine makeElectricEngine(ElectricEngine engine); }
GasEngineModule模块换成ElectricEngineModule,
@Component(modules = {ElectricEngineModule.class})//替换 public interface CarGraph { //... }
运行就能得到电动车了,可见这个过程我们不需要修改NewCar就能换掉引擎(偷梁换柱)。
@IntoMap和@StringKey
又有一天,突然想开双混动汽车了咋整,就是说汽油引擎和电动引擎我全都要,我们发现@Component的modules可以传入数组,那试试把两个引擎模块都传进去,
@Component(modules = {GasEngineModule.class, ElectricEngineModule.class}) public interface CarGraph { //... }
make一下,不用想也知道会出错,NewCar只有一个依赖IEngine接口,这时dagger已经不知道,造NewCar到底要传入哪个引擎了,
那怎么办?支持多绑定的@IntoMap和@StringKey登场了。
IntoMap表示会把这个Module存进map里,StringKey表示Module名字,这个名字也会作为map的key,
@Module abstract class GasEngineModule {//汽油引擎模块 @Binds @IntoMap @StringKey("Gas") abstract IEngine makeGasEngine(GasEngine engine); } @Module abstract class ElectricEngineModule {//电动引擎模块 @Binds @IntoMap @StringKey("Electric") abstract IEngine makeElectricEngine(ElectricEngine engine); }
然后修改一下NewCar,此时不再只是依赖一个引擎接口IEngine了,而是一组引擎,
class NewCar { //我现在是混动车了,要用map接收引擎 private final Map<String, IEngine> mEngineMap; @Inject //告诉dagger如何创建NewCar public NewCar(Map<String, IEngine> engineMap) { mEngineMap = engineMap; } public void start() { //双混动走起~ for (Map.Entry<String, IEngine> entry : mEngineMap.entrySet()) { entry.getValue().start(); //汽油 引擎发动,前往秋名山 //电动 引擎发动,前往秋名山 } } }
Activity代码不用改动,双混动车就造好了。
@Singleton
如果想要给双混动NewCar限量全球一款,可以用@Singleton指定为单例,
@Singleton //单例,我现在是限量款混动车 public class NewCar { //... } @Singleton //造车图纸也需跟着单例 @Component(modules = {GasEngineModule.class, ElectricEngineModule.class}) public interface CarGraph { //... }
Activity运行,
//DaggerActivity.java void onCreate(Bundle savedInstanceState) { CarGraph carGraph = DaggerCarGraph.create(); NewCar newCar = carGraph.makeNewCar(); newCar.start(); NewCar newCar2 = carGraph.makeNewCar(); newCar2.start(); //newCar和newCar2是同一个实例 Log.e("哈利迪", newCar.hashCode() + "," + newCar2.hashCode()); }
dagger的使用就先聊到这啦,相信对dagger也已经有了初步认识,还有些注解没讲到,比如:
@Provides:当我们没法用@Inject来标记实例的创建姿势时,可以用@Module和@Provides来提供实例,比如Retrofit是三方库的类我们没法标记其构造方法,则可以用Provides提供,
@Module public class NetworkModule { @Provides public Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("xxx") .build(); } }
@Scope:作用域 ...
@Subcomponent:子组件...
注解还有很多,准备放到细枝篇来写了...🤣
实现原理
dagger编译期解析注解创建辅助类的过程就不分析了,我们直接看他生成的辅助类,
注:一开始写接口名字时,用造车图纸CarGraph而不是造车厂CarFactory,是为了避免和dagger的生成类搞混,用CarGraph有几何图的寓意,可以理解成造车蓝图(PPT),让我们一起,为梦想窒息...
我们看到DaggerCarGraph,他实现了我们的CarGraph接口,
class DaggerCarGraph implements CarGraph { //Provider,提供多引擎map,<引擎名字、引擎实例> private Provider<Map<String, IEngine>> mapOfStringAndIEngineProvider; //Provider,提供NewCar private Provider<NewCar> newCarProvider; private DaggerCarGraph() { initialize(); } public static Builder builder() { return new Builder(); } public static CarGraph create() { //用builder创建DaggerCarGraph实例 return new Builder().build(); } private void initialize() { //创建提供汽油引擎的Provider和电动引擎的Provider //用put将他们存起来,合并成一个提供多引擎map的Provider this.mapOfStringAndIEngineProvider = MapFactory.<String, IEngine>builder(2) .put("Gas", (Provider) GasEngine_Factory.create()) .put("Electric", (Provider) ElectricEngine_Factory.create()) .build(); //把提供多引擎map的Provider传入NewCar_Factory, //然后用DoubleCheck包一层,他会加锁、处理单例逻辑 this.newCarProvider = DoubleCheck.provider( NewCar_Factory.create(mapOfStringAndIEngineProvider)); } @Override public Car makeCar() { //1. 老的造车:姿势一,用makeCar直接造 return new Car(new Engine()); } @Override public void inject(DaggerActivity activity) { //2. 老的造车:姿势二,用@Inject注入 ↓ injectDaggerActivity(activity); } private DaggerActivity injectDaggerActivity(DaggerActivity instance) { //2. 老的造车:姿势二,先创建后注入 //实例的创建也用makeCar,如果我们接口没有定义这个方法,dagger会生成一个功能一样的getCar DaggerActivity_MembersInjector.injectMCar(instance, makeCar()); return instance; } @Override public NewCar makeNewCar() { //3. 新的造车,从Provider获取 return newCarProvider.get(); } public static final class Builder { private Builder() { } public CarGraph build() { return new DaggerCarGraph(); } } }
- 老的造车:姿势一,用makeCar直接造
在造老车Car时,姿势一是直接调接口的makeCar方法来造,实现就是简单的new出实例。
- 老的造车:姿势二,用@Inject注入
姿势二用@Inject注入实例,可见他也是先调makeCar()得到实例,然后调DaggerActivity_MembersInjector.injectMCar进行注入,
//DaggerActivity_MembersInjector.java @InjectedFieldSignature("com.holiday.srccodestudy.dagger.DaggerActivity.mCar") public static void injectMCar(DaggerActivity instance, Car mCar) { //引用DaggerActivity的成员,为其赋值,可见mCar不能声明为private instance.mCar = mCar; }
- 新的造车,从Provider获取
在造新车NewCar时,是从Provider获取的,跟进newCarProvider.get(),如果使用了单例@Singleton,NewCar_Factory会被DoubleCheck包一层,DoubleCheck会加锁和处理单例逻辑,我们直接看NewCar_Factory的get就行了,
//Factory<T> extends Provider<T> class NewCar_Factory implements Factory<NewCar> { private final Provider<Map<String, IEngine>> engineMapProvider; public NewCar_Factory(Provider<Map<String, IEngine>> engineMapProvider) { this.engineMapProvider = engineMapProvider; } @Override public NewCar get() { //通过engineMapProvider获取多引擎map return newInstance(engineMapProvider.get()); } public static NewCar_Factory create(Provider<Map<String, IEngine>> engineMapProvider) { return new NewCar_Factory(engineMapProvider); } public static NewCar newInstance(Map<String, IEngine> engineMap) { //new出新车实例 return new NewCar(engineMap); } }
看下多引擎map又是如何获取的,engineMapProvider.get(),
//MapFactory.java //MapFactory extends AbstractMapFactory implements Factory public Map<K, V> get() { //创建新的LinkedHashMap Map<K, V> result = newLinkedHashMapWithExpectedSize(contributingMap().size()); //遍历已存入的引擎,存进新map for (Entry<K, Provider<V>> entry : contributingMap().entrySet()) { result.put(entry.getKey(), entry.getValue().get()); } //返回不可修改的map return unmodifiableMap(result); }
至此,dagger的各生产线流程就分析完了,
细枝
@Scope作用域、@Subcomponent子组件、还有SPI(Service Provider Interface)、grpc(谷歌的远程过程调用框架)等,都留到细枝篇来写啦。
使用场景
那dagger在Android中有哪些用武之地?首先是从架构角度
,在谷歌示例中,结合了Activity、伪ViewModel、Repository、DataSource和Retrofit来使用dagger,(还没用过,不知道香不香、坑多不多,靠屏幕前的大佬们反馈了~)
然后我们在项目中的一些场景,是从业务角度
切入,在个别复杂度较高的业务线上单独使用dagger。例如钱包业务,有大量实例
和大量页面/视图
存在多对多关系,比如钱包Act需要钱包Api、钱包用户信息Manager;充值Act需要支付Api、充值Service;银行卡列表View需要银行Service...像这种多对多、对象依赖关系杂乱无章的场景,很适合用dagger来帮我们注入。
@PurseScope //钱包作用域 @Component(modules = PurseModule.class) //由PurseModule提供实例 public interface PurseComponent { //为act注入 void inject(退款详情Act xxxAct); void inject(银行卡首页Act xxxAct); void inject(钱包首页Act xxxAct); void inject(钱包设置Act xxxAct); void inject(余额首页Act xxxAct); //... //为view注入 void inject(钱包首页GridContainer xxxGridContainer); void inject(银行卡列表View xxxView); //提供网络调用 支付Api xxxApi(); //提供钱包用户信息 钱包用户信息Manager xxxManager(); //... }
然后看到PurseModule,用于提供各种实例,可以new、手动单例、工厂生产...
@Module public class PurseModule { @Provides @PurseScope public 支付Api provide支付Api(...) { return new 支付Api(...); } @Provides @PurseScope public 用户信息Manager provide用户信息Manager(...) { return 用户信息Manager.get(...); } @Provides @PurseScope public 充值Service provide充值Service(...) { return new 充值ServiceFactory.create(...); } //... }
尾声
简单总结下dagger的优缺点~
- 优势:无反射、支持增量编译
- 缺点:构建过程需要而外的io和编译时间、生成类会增大包体积、不够好用、
后续计划:dagger细枝、hilt、koin、顺便看看spring上的注入@Autowired是怎么做的...学海无涯啊~
系列文章:
参考资料
- GitHub & 文档 & API
- 谷歌 - Android 中的依赖项注入
- 谷歌 - 在 Kotlin 中使用 Dagger 会遇到的陷阱和优化方法
- 掘金 - 从Dagger到Hilt,谷歌为何执着于让我们用依赖注入