核心概念
| 概念 | 范畴 | 关注点 | 意义 |
|---|---|---|---|
| 控制反转(IoC) | 设计思想 | 谁控制依赖获取 | 解耦,提高灵活性 |
| 依赖注入(DI) | IoC实现方式 | 如何得到依赖 | 被动获取依赖 |
| 依赖查找(DL) | IoC实现方式 | 如何得到依赖 | 主动获取依赖 |
| 依赖倒置原则(DIP) | 设计原则 | 依赖方向、高层与低层 | 面向抽象编程解耦 |
依赖倒置原则:高层模块不应该依赖底层模块,二者都该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象
依赖注入框架
依赖注入是用于实现控制反转的方式之一,另外一种实现控制反转的方式是依赖查找
依赖查找/服务发现
public class Man {
Car car = ServiceManager.get(Car);
public Man() { }
}
常见的依赖注入的实现方式
- 基于构造方法 或 setter 方法
public class Man {
Car car;
public Man(Car car) {
this.car = car;
}
}
public class Man {
public void setCar(Car car) {
this.car = car;
}
}
- 基于注解
由框架来帮助构建实例,依赖图更加清晰
public class Man {
@Inject
Car car;
}
依赖注入在工程上带来的好处主要有:
- 代码复用性更好
- 更容易重构
- 更容易测试
Dagger2
Dagger 2 是专门为 Android 开发设计的一款依赖注入框架。在设计上遵循 JSR 330 标准,通过注解生成代码的方式来实现依赖注入。一方面避免了用户创建大量的模版代码,另一面避免了运行时通过反射的方式调用带来的性能问题。同时编译期间的依赖检查,使得问题能够提前暴露,易于追踪
Dagger2需要感知具体类型,标注子类构造方法无法赋值给父类。由于Dagger2有明确的依赖推导,因此可以方便的使用APT来生成辅助类,而无需依赖Transformer
原理如下:
package com.example.lib;
import com.google.auto.service.AutoService;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.SourceVersion;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@AutoService(Processor.class)
public class CustomAnnotationProcessor extends AbstractProcessor {
private Filer mFiler;
private Messager mMessager;
private Elements mElementUtils;
private Set<? extends Element> annotatedElements;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
mElementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//通过入口解析所有需要的类 -> com.example.library.MengDeng125
TypeElement typeElement = mElementUtils.getTypeElement("com.example.mylibrary.MengDeng125");
// 获取类中所有的元素(包括方法、字段等)
List<? extends Element> enclosedElements = typeElement.getEnclosedElements();
for (Element element : enclosedElements) {
// 判断是否为方法
if (element.getKind() == ElementKind.CONSTRUCTOR) {
ExecutableElement methodElement = (ExecutableElement) element;
// 获取方法名
String methodName = methodElement.getSimpleName().toString();
// 获取方法的注解信息
MyAnnotation annotation = methodElement.getAnnotation(MyAnnotation.class);
if (annotation != null) {
// 处理注解信息
System.out.println("Found method annotated with MyAnnotation: " + methodName);
}
}
}
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> sets = new HashSet<>();
sets.add(MyAnnotation.class.getCanonicalName());
return sets;
}
// @Override
// public SourceVersion getSupportedSourceVersion() {
// return SourceVersion.RELEASE_6;
// }
}
基本用法
- 使用 @Inject 标注需要注入的变量或方法
- 使用 @Inject 标注构造函数来完成实例的创建
- 使用 @Module 来完成实例的创建
- 使用 @Component 标注接口或抽象类,Component 桥梁可以完成依赖注入过程
Lazy & Provider
Lazy<T>类可以实现延迟注入,降低内存开销(DoubleCheck,缓存 Instance 实例),只在第一次调用时创建实例,但由于 Instance 实例不是静态的,所以不是单例模式
class Monkey @Inject constructor() {
@Inject
lateinit var stick1: Lazy<Stick>
@Inject
lateinit var stick2: Lazy<Stick>
@Test
fun compare() {
assert(stick1.get() == stick2.get()) // false
}
}
Provider<T>类每次调用都会创建新的实例
Provider 和 Lazy 结合使用:Provider<Lazy<Stick>>,这样既可以保证效率,也可以保证能获取到多个实例
Qualifier
对于同一类型有多个实例需要注入的情况,通过限定符 @Qualifier 可以防止依赖迷失。系统提供了默认的 @Name 限定符,但是不是强类型的,我们也可以通过 Qualifier 来生成自定义的限定符
Scope
相当于单例模式,对于同一个 Component,每次调用 Component.inject 或 Component.getXxx 方法内部维护的都是同一个对象。这引出了 Component 分层的概念,Component 有两种分层方式:继承、依赖(如果不想全部暴露,可以选择依赖关系)
依赖关系 vs 继承关系
-
相同点
- 两者都能复用其他 Component 的依赖
- 有依赖关系和继承关系的 Component 不能有相同的 Scope
-
不同点
- 依赖关系中被依赖的 Component 必须显式地提供公开依赖实例的接口,而 SubComponent 默认继承 parent Component 的依赖
- 依赖关系会生成两个独立的 DaggerXXComponent 类,而在继承关系中,SubComponent 需要声明 @Subcomponent.Builder 标注的 builder 方法,SubComponent 不会生成独立的 DaggerXXComponent 类
// 依赖关系
@CustomScope
@Component(modules = [AnimalModule::class], dependencies = [BaseComponent::class])
interface ActivityComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun bindCow(cow: Cow): Builder
fun bindBaseComponent(baseComponent: BaseComponent): Builder
fun build(): ActivityComponent
}
fun injectDaggerActivity(activity: DaggerActivity)
}
@BaseScope
@Component(modules = [RealPeople::class, PeopleModule::class])
interface BaseComponent {
fun man(): Man
fun woman(): Woman
fun people(): People
}
// 继承关系
// @Module 注解除了可以用于声明依赖的提供方法(@Provides),还可以通过 subcomponents 属性,指定该 module 参与哪些 Subcomponents 的构建
// 这样声明后,component graph 会自动知道如何组装这些 Subcomponent,无需再在 @Component 上手动声明它们
@Module(subcomponents = [SonComponent.class])
public class CarModule {
@Provides
@ManScope
static Car provideCar() {
return new Car();
}
}
@SonScope
@SubComponent(modules = BikeModule.class)
public interface SonComponent {
void inject(Son son);
@Subcomponent.Builder
interface Builder {
SonComponent build();
}
}
@ManScope
@Component(modules = CarModule.class)
public interface ManComponent {
void injectMan(Man man);
SonComponent.Builder sonComponent(); // 用来创建 Subcomponent
}
手动注入
class Monkey {
@Inject
lateinit var stick: Stick
}
在上面这个例子中,Monkey 需要一个 Stick,但是 Monkey 并没有被 @Inject 注解的构造方法,也没有通过 Module 创建 Monkey 实例,所以 Dagger 框架无法构建 Monkey 实例,这时就需要使用到 Dagger 的手动注入能力
@Component
interface MonkeyComponent {
fun inject(monkey: Monkey)
}
val component = DaggerMonkeyComponent.create()
val monkey = Monkey()
component.inject(monkey) // 手动注入
val stick = monkey.stick
手动注入适用于 UI 入口类等无法手动创建的实例,但是也存在缺点:
Component 和 Monkey(UI 组件)需要在一起编译,这导致了服务提供方依赖了服务调用方,但在实际场景中,我们是不知道服务调用方的存在,这就引出了手动获取的方式
手动获取
@Component
interface MonkeyComponent {
fun stick(): Stick
}
val component = DaggerMonkeyComponent.create()
val stick = component.stick
- 手动注入可以获取到 Dagger 中管理的所有对象,但 Dagger 需要感知到被注入对象的存在
- 手动获取只能获取到 Dagger 主动暴露的对象,比较安全
- 手动注入适用于一些内部场景,既是提供方也是使用方。手动获取实例主要用于三方 SDK 对外提供能力,需要限制外部对内部的访问
外部注入
class Monkey {
@Inject
lateinit var stick: Stick
@Inject
lateinit var companyName: String
}
由于 Monkey 也需要工作,而具体的公司需要外部来指定,这时我们就需要外部注入的功能
@Component
interface MonkeyComponent {
fun stick(): Stick
@Component.Builder
interface Builder {
@BindInstance
fun companyName(name: String): Builder
fun build(): MonkeyComponent
}
}
MultiBinding
对应两个注解:IntoSet、IntoMap
Hilt
系统提供了默认的注入点 @AndroidEntryPoint,目前支持的有:Application、Activity、Fragment、View、Service、BroadcastReceiver
Hilt 是由 Google 推出的基于 Dagger 2 的依赖注入框架,旨在简化 Android 应用中的依赖注入流程。下面是 Hilt 和 Dagger 2 之间的一些主要区别和使用场景:
-
简化配置:
- Dagger 2 需要开发人员手动编写大量的代码来配置和连接依赖项。这包括编写模块、组件、限定符等。
- Hilt 提供了注解处理器,可以自动生成大部分 Dagger 2 中需要手动编写的代码,使得配置变得更加简单和直观。
-
减少模板代码:
- 在 Dagger 2 中,需要编写很多模板代码来连接各个依赖项和提供全局依赖项。
- Hilt 提供了注解和生成器,可以自动生成大部分模板代码,使得开发者只需专注于业务逻辑,而无需编写重复的模板代码。
-
注解简化:
- Dagger 2 的注解相对较复杂,需要开发人员对其有一定的了解才能正确地使用。
- Hilt 提供了一套更加简化的注解,使得开发者更容易理解和使用依赖注入功能。
-
Android 特化:
- Hilt 被设计成更适合在 Android 应用中使用,提供了特定于 Android 的功能,如与 Android 生命周期的集成、支持 ViewModel 的依赖注入等。
- Dagger 2 是一个通用的依赖注入框架,可以用于任何 Java 或 Android 应用,但是需要开发者自行处理 Android 特定的问题。
使用场景:
- 如果你是一个 Android 开发者,并且希望简化你的应用中的依赖注入流程,那么可以考虑使用 Hilt。
- 如果你的项目比较复杂,需要更多的灵活性和控制,或者已经熟悉了 Dagger 2,并且不想引入新的依赖库,那么可以继续使用 Dagger 2。
ARouter & Dagger2
如果你主要关注的是实现组件之间的页面跳转、参数传递以及拦截器等路由功能,那么 ARouter 可能更适合你的需求。ARouter 提供了简单易用的 API,能够方便地实现这些功能,并且在阿里巴巴内部得到了广泛的应用,有着较为成熟的技术支持和社区。
而如果你更关注的是依赖注入方面的功能,例如管理组件之间的依赖关系、提高代码的可测试性和可维护性等,那么 Dagger2 可能更适合你的需求。Dagger2 是一个强大的依赖注入框架,能够帮助你管理复杂的依赖关系,并且通过依赖注入的方式来实现解耦,提高代码质量。
在实际项目中,你也可以同时使用 ARouter 和 Dagger2,根据具体的需求选择合适的工具来解决问题。比如,你可以使用 ARouter 来管理页面跳转和路由功能,同时使用 Dagger2 来管理依赖关系,以此来充分发挥它们各自的优势。
参考文献