Android | dagger细枝篇
嗨,我是哈利迪~《看完不忘系列》之dagger(树干篇)一文对dagger做了初步介绍,下面我们一起来瞅瞅dagger的一些细节。
本文约3.5k字,阅读大约9分钟。
Dagger源码基于最新版本2.28.3
目录:
@Binds和@Provides区别
Binds和Provides都是搭配Module(模块,用于支持模块化)使用的,Binds表示抽象和具体的绑定,作用在抽象方法上,如下,
@Module abstract class GasEngineModule {//汽油引擎模块 @Binds abstract IEngine makeGasEngine(GasEngine engine); //抽象的IEngine接口作为返回值,具体的GasEngine作为入参,形成绑定关系 //当有地方依赖了IEngine时,这里可以为他提供GasEngine实例 }
为什么用抽象方法,因为这里我们要做的只是声明绑定关系,dagger根据声明就知道如何提供实例了,dagger不会调用这个方法或为他生成具体实现。
当需要提供的实例不是一个而是一组时,Binds可以和集合注解:@IntoSet、@ElementsIntoSet、@IntoMap一起使用,
@Module abstract class GasEngineModule {//汽油引擎模块 @Binds @IntoMap //支持入map @StringKey("Gas") //使用IntoMap的话,还需要指定一个key abstract IEngine makeGasEngine(GasEngine engine); } @Module abstract class ElectricEngineModule {//电动引擎模块 @Binds @IntoMap //支持入map @StringKey("Electric") abstract IEngine makeElectricEngine(ElectricEngine engine); }
key的指定也有多种类型,如@StringKey、@IntKey、@LongKey、@ClassKey等。
而Provides就比较简单了,当我们没法用@Inject来标记实例的创建姿势时,可以用@Provides来提供实例,比如Retrofit是三方库的类我们没法标记其构造方法,则可以用Provides提供,
@Module public class NetworkModule { @Provides public Retrofit provideRetrofit() { return new Retrofit.Builder() .baseUrl("xxx") .build(); } }
可见Binds用于声明接口和实例的绑定关系
,Provides用于直接提供实例
,他们都写在可模块化的Module里。
然后经过Component指定modules,这些声明就能被dagger找到了,
@Component(modules = {GasEngineModule.class, ElectricEngineModule.class}) //传入 public interface CarGraph { //... }
@Subcomponent
Subcomponent即子组件,可以将父组件的能力继承过来,同时扩展自己的能力。现在已经有了@Component的CarGraph来造车,我们可以建一个@Subcomponent的WheelGraph来造轮胎,(虽是轮胎图纸,但也有造车之心)
@Subcomponent //子组件,轮胎图纸 public interface WheelGraph { Wheel makeWheel(); //造轮胎 @Subcomponent.Factory //告知CarGraph如何创建WheelGraph interface Factory { WheelGraph create(); } }
下面要告知dagger WheelGraph是CarGraph的子组件,需要提供Module,借助Module将子组件的类WheelGraph传给subcomponents,
@Module(subcomponents = WheelGraph.class) public class WheelModule { }
将Module添加到CarGraph,
@Component(modules = {WheelModule.class}) public interface CarGraph { //... //提供接口,让使用者能借助Factory拿到WheelGraph实例 WheelGraph.Factory wheelGraph(); }
使用,
//轮胎图纸 WheelGraph wheelGraph = carGraph.wheelGraph().create(); //造轮胎 Wheel wheel = wheelGraph.makeWheel();
然后我们看下子组件是如何使用父组件的能力的,在WheelGraph里添加一个inject,
@Subcomponent public interface WheelGraph { //... //告诉dagger谁要注入 void inject(DaggerActivity activity); }
把DaggerActivity里的 carGraph.inject(this) 换成 wheelGraph.inject(this),发现mCar也能正常运行,
class DaggerActivity extends AppCompatActivity { @Inject Car mCar; void onCreate(Bundle savedInstanceState) { CarGraph carGraph = DaggerCarGraph.create(); //carGraph.inject(this); WheelGraph wheelGraph = carGraph.wheelGraph().create(); wheelGraph.inject(this); //正常发动 mCar.start(); } }
可见wheelGraph把carGraph的造车能力也给继承过来了。
我们看到生成的DaggerCarGraph类,里面有个内部类WheelGraphImpl(子组件不会生成单独的DaggerXXX类,而是依附于父组件),
class WheelGraphImpl implements WheelGraph { @Override public Wheel makeWheel() { return new Wheel(); } @Override public void inject(DaggerActivity activity) { injectDaggerActivity(activity);// ↓ } private DaggerActivity injectDaggerActivity(DaggerActivity instance) { //跟父组件的注入逻辑一致,还调用了父组件的造车能力DaggerCarGraph.this.makeCar() DaggerActivity_MembersInjector.injectMCar(instance, DaggerCarGraph.this.makeCar()); return instance; } }
谷歌示例会形象些,ApplicationComponent和LoginComponent就是父与子的关系,LoginComponent自己管理LoginViewModel,但同时又继承了父组件的数据获取能力,
@Scope
Scope即作用域,是一种元注解,即作用于注解的注解。@Singleton可以实现全局单例,当父组件CarGraph标记了@Singleton,子组件WheelGraph就不能使用相同的@Singleton了,我们可以自定义注解。
@Scope //元注解 @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope { //表示我想要一个Activity级别的作用域 }
使用,
@ActivityScope //使用自定义注解 public class Wheel { } @ActivityScope //使用自定义注解 @Subcomponent public interface WheelGraph { }
@Qualifier
Qualifier即限定符,也是一种元注解。当有多个相同类型的实例要提供时,比如轮胎有最小和最大尺寸都是相同的int类型,
public class Wheel { int minSize; int maxSize; @Inject public Wheel(int minSize, int maxSize) { this.minSi***Size; this.maxSize = maxSize; } }
WheelModule进行提供,
@Module(subcomponents = WheelGraph.class) public class WheelModule { @Provides static int minSize() { return 12; } @Provides static int maxSize() { return 23; } }
这时dagger就会出错了,他不知道多个int的对应关系,需要自定义注解来指定,
@Qualifier //元注解 @Retention(RUNTIME) public @interface MinSize {} //限定符注解 @Qualifier //元注解 @Retention(RUNTIME) public @interface MaxSize {} //限定符注解
然后在Wheel和WheelModule都加上限定即可
public class Wheel { int minSize; int maxSize; @Inject public Wheel(@MinSize int minSize, @MaxSize int maxSize) {//使用自定义注解 this.minSi***Size; this.maxSize = maxSize; } } @Module(subcomponents = WheelGraph.class) public class WheelModule { @Provides @MinSize //使用自定义注解 static int minSize() { return 12; } @Provides @MaxSize //使用自定义注解 static int maxSize() { return 23; } }
SPI机制
dagger文档里提到了SPI机制,哈迪蹩脚的英文看得一头雾水,不过最近刚好看到了好友帅丙的文章Dubbo的SPI机制,做了个简单的理解,SPI(Service Provider Interface,服务提供者接口)简单来说,就是先统一接口,然后按需扩展自己的实现类,到了运行的时候再去查找加载对应的实现类,不过这里的实现类,我们集中放在一个事先约定好的地方。
比如MySQL,
约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。
这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。
-- 敖丙
声明好了实现类后,运行时就会有java自带的ServiceLoader来解析文件,加载实现类(不过Dubbo为了优化自建了一套)。
再比如我们更为熟悉的gradle-plugin开发,也会约定一个地方来声明实现类,在插件工程的resources/META-INF/gradle-plugins/目录下建一个plugin_name.properties
文件,声明类的全名,(参见butterknife)
implementation-class=butterknife.plugin.ButterKnifePlugin
所以SPI简单理解就是一种可扩展的、提供实现类的约定机制
。那么SPI对dagger来说有啥用途呢?
注意:dagger的SPI还是实验性的,随时可变
我们看到dagger的spi包下的一个接口BindingGraphPlugin,
//我是一个插件接口,可以插在dagger图绑定(创建类连接依赖关系)的过程中 public interface BindingGraphPlugin { //为Dagger处理器遇到的每个有效根绑定图调用一次。 可以使用diagnosticReporter报告诊断 void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter); //使用Filer初始化此插件,该Filer可用于基于绑定图编写Java或其他文件。 //在访问任何图形之前,此插件的每个实例将调用一次 default void initFiler(Filer filer) {} //... default void initTypes(Types types) {} //... default void initElements(Elements elements) {} //... default void initOptions(Map<String, String> options) {} //返回此插件用于配置行为的注释处理选项。 default Set<String> supportedOptions() { return Collections.emptySet(); } //插件的唯一名称,将在打印到Messager的诊断程序中使用。 默认情况下,使用插件的标准名称 default String pluginName() { return getClass().getCanonicalName(); } }
咦,BindingGraphPlugin是啥玩意?你想想,再好好想想,他是不是长得很像我们开发注解处理器APT时做的一些事情,看下ButterKnifeProcessor,
@AutoService(Processor.class) //使用AutoService自动帮我们做SPI声明, //他会创建META-INF/services/javax.annotation.processing.Processor文件,帮我们写入注解处理器的类全名 public final class ButterKnifeProcessor extends AbstractProcessor { public void init(ProcessingEnvironment env) {} public Set<String> getSupportedOptions() {} public Set<String> getSupportedAnnotationTypes() {} public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {} }
怎么样,长得是不是还挺像,那就很好理解了,注解处理器在构建时插入一段逻辑来解析注解生成辅助类,那dagger的BindingGraphPlugin就是在图绑定阶段插一段逻辑来做自定义处理嘛。
dagger内置了一些实现类,如DependencyCycleValidator用来检测是否有循环依赖、FailingPlugin用来生成错误报告、InjectBindingValidator用于验证@Inject标记的构造方法...
如果有特殊需求,就可以自定义插件,插入自己的逻辑,例如实现以下功能:
- 添加项目特定的错误和警告,例如android.content.Context的绑定必须具有@Qualifier或实现DatabaseRelated的绑定必须是作用域
- 生成额外的Java源文件
- 将Dagger模型序列化为资源文件
- 建立Dagger模型的可视化(可将绑定图(依赖关系)转成json/proto,然后渲染成UI)
-- dagger
自定义插件使用@AutoService(BindingGraphPlugin.class)进行SPI声明,例如dagger的一个TestPlugin,
@AutoService(BindingGraphPlugin.class) public final class TestPlugin implements BindingGraphPlugin { private Filer filer;//文件操作 @Override public void initFiler(Filer filer) { this.filer = filer; } @Override public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { //定制逻辑... } }
定义好插件后,dagger就会使用java自带的ServiceLoader进行查找和加载,在SpiModule中,
@Module abstract class SpiModule { private SpiModule() {} @Provides @Singleton static ImmutableSet<BindingGraphPlugin> externalPlugins( @TestingPlugins Optional<ImmutableSet<BindingGraphPlugin>> testingPlugins, @ProcessorClassLoader ClassLoader processorClassLoader) { return testingPlugins.orElseGet( () -> ImmutableSet.copyOf( //ServiceLoader.load ServiceLoader.load(BindingGraphPlugin.class, processorClassLoader))); } }
对ServiceLoader.load实现感兴趣的读者,可以看敖丙的文章Dubbo的SPI机制,实现不是很复杂。
延伸
延伸部分略枯燥,纯属个人装B学习记录用,不感兴趣可跳过~🤪
偷偷装个B,哈迪在公司其实脚本和后端都写过(当然只是内部的管理平台,不可能是核心业务啦),所以分析技术时已经习惯了各种发散和对比,越发觉得其实很多端的技术很是相通~看过Tangram系列的读者应该也见过TangramService项目,这还是用大学时搭的壳来写的...
@Autowired注入
源码基于spring 4.3.13,spring boot 1.5.9
使用姿势,
@RestController public class ActivityController { @Autowired //自动注入 private ActivityService activityService; } @Service public class ActivityService {} //注意:因为代码不多,所以就没按规范抽成接口和Impl
那么activityService实例是如何自动注入的呢?
Spring会扫描@Component注解的Bean,然后AbstractAutowireCapableBeanFactory#populateBean负责注入bean的属性,他里边调了ibp.postProcessPropertyValues,我们看到AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues,
//AutowiredAnnotationBeanPostProcessor.java public PropertyValues postProcessPropertyValues() { //1.获取Class中关于属性注入相关注解的元数据 InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); //2.完成属性注入操作 metadata.inject(bean, beanName, pvs); return pvs; }
1.findAutowiringMetadata主要是做检查和缓存,他调了buildAutowiringMetadata,跟进,
//AutowiredAnnotationBeanPostProcessor.java private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) { LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<InjectionMetadata.InjectedElement>(); //目标类,如我们的ActivityController Class<?> targetClass = clazz; do { //收集 final LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<InjectionMetadata.InjectedElement>(); //记录有@Autowired,@Value等注解的field ReflectionUtils.doWithLocalFields(targetClass, new ReflectionUtils.FieldCallback() { @Override public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { AnnotationAttributes ann = findAutowiredAnnotation(field); if (ann != null) { //... currElements.add(new AutowiredFieldElement(field, required)); } } }); //记录有@Autowired,@Value等注解的method ReflectionUtils.doWithLocalMethods(targetClass, new ReflectionUtils.MethodCallback() { @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { //... AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod); if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { //... currElements.add(new AutowiredMethodElement(method, required, pd)); } } }); //收集 elements.addAll(0, currElements); //继续往上查找父类 targetClass = targetClass.getSuperclass(); }while (targetClass != null && targetClass != Object.class); //返回包装好的元数据 return new InjectionMetadata(clazz, elements); }
解析类的注解得到包装好的元数据后,就调用2.metadata.inject进行注入,他又调了element.inject,我们直接看到AutowiredFieldElement#inject,
//AutowiredFieldElement protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable { //根据元数据,解析属性值,找到我们的ActivityService实例 //DefaultListableBeanFactory.resolveDependency -> doResolveDependency -> //findAutowireCandidates -> addCandidateEntry -> candidates.put -> //descriptor.resolveCandidate -> AbstractBeanFactory.getBean -> getObjectForBeanInstance... value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); //设置可访问 ReflectionUtils.makeAccessible(field); //将实例注入 field.set(bean, value); }
可见@Autowired的注入是基于反射
来做的~
gRPC
RPC(Remote Procedure Call)是远程过程调用
,后端进行服务化后,通常会在一个主机上调用另一个主机提供的方法,比如获取一个用户收藏的商品,用户服务器就会调用商品服务器的方法,什么?居然还能调用远程主机应用的方法?其实可以这么理解,就是基于一定协议和数据格式进行通信
,比如http+json/proto,但是直接这么用不方便,就封装成接口方法的方式来调用了,看起来是调了一个方法,其实内部进行了远程通信。像Dubbo、Spring Cloud、gRPC就是一些远程过程调用的框架。可以类比Android的跨进程通信IPC,只不过RPC不仅跨了进程,还跨了主机。
gRPC是谷歌开源的高性能远程过程调用框架,dagger文档的gRPC就寥寥几句,还不完善,看不出个所以然,就先不看了,简单了解下就行...
尾声
由于哈迪没有dagger实战经验,写起来还是有点吃力的,写着写着就有种搬文档的感觉...而且dagger的使用程度貌似也不怎么高,真要用得炉火纯青,可能还得要靠深刻理解业务,能够发掘契合度极高的业务高手了吧...怎么说呢,趁年轻好奇心足够强烈,多学点还是好的,我们下期见~🤔
系列文章: