12 依赖注入与路由:框架能够为我们解决什么问题?
你好,我是黄俊彬。
上节课我们对Sharing项目做了组件化架构重构。在此过程中,页面之间的跳转和接口的实现注入,我们都用到了反射来解耦,既然使用反射也能解决耦合的问题,为什么我们还要使用路由及注入框架呢?它们能给我们带来什么帮助呢?
这节课,我将和你一起学习路由及注入框架的设计思想以及实现原理,然后结合这些框架进一步改造Sharing项目。
使用框架的意义
想理解使用框架的意义,我们不妨先对比一下用反射来解耦是什么情况。
在上节课中为了解耦消息组件与文件组件对账户组件的依赖,我们提取了IAccountState接口,并使用反射加载对应的实现。
public class FileController {
private IAccountState iAccountState;
{
try {
iAccountState = (IAccountState) Class.forName("com.jkb.junbin.sharing.feature.account.AccountStateImpl").newInstance();
} catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
e.printStackTrace();
}
}
//... ...
}
我们再看看IAccountState的引用情况。
如上图所示,IAccountState被多个类引用,在非常多的地方都需要进行反射的操作。
你应该也发现目前的问题了,那就是有很多重复的代码,并且这些代码都是一些非业务的模板代码。那我们可以将IAccountState的创建封装成一个单独的方法,减少重复代码吗?当然可以,但是如果项目里面有几百个接口,那我们也得去封装几百个方法,这些同样是非业务的模板代码。
所以这个时候框架就可以派上用场了。框架的意义是帮我们统一管理项目中的非业务模板代码,提供更灵活的扩展方式,让我们可以聚焦在业务功能的代码实现上。
下面我们以注入和路由框架为例,一起来感受一下使用框架带来的变化。
依赖注入
依赖注入(DI)是一种软件设计模式,也是实现控制反转的其中一种技术。这种模式能让一个对象接收它所依赖的其他对象。
在编写代码中,我们经常会遇到一种情况,就是一个类需要引用其他类。例如,前面例子中FileController类可能需要引用IAccountState类,这些必需类称为依赖项,FileController类需要依赖IAccountState实例才能运行。
通常情况下,类可以通过以下三种方式获取所需的对象:
- 类构造其所需的依赖项。例如从构造方法将实现注入进来。
- 以参数形式提供。例如前面LogUtils将usename的依赖提取为参数。
- 以set方法赋值。例如前面MainActivity提供了setFileAddClickListener方法。
使用这些方法,不必让类实例自行获取依赖。
想要实现依赖注入,我们可以使用手工管理注入的方式,也就是通过上述3种方式将依赖传递进来,但这样我们同样需要去维护大量的非业务模板代码,特别是当依赖需要层层传递时,代码的可维护性就会非常差。所以通常在项目中,我们都会使用统一的依赖注入框架来管理对象间的依赖关系。
使用依赖注入,常见的方式有2种:一种是静态注入,就是前面提到的通过构造函数及参数等方式直接通过代码注入依赖,静态注入是在编译时连接依赖项的代码;另外一种是动态注入,最常见的方式就是通过反射的机制,动态注入是在运行时连接依赖的代码。
这2种注入方式的对比,你可以参考后面的表格。
对于Sharing项目来说,首先考虑到后续的组件可能会动态加载,所以我们采用动态注入的框架来实现。另外后续也会集成路由框架,所以采用ARouter框架来统一管理注入和路由功能。
接下来,我们一起来看看如何使用Arouter框架来管理代码的依赖注入。
首先需要在gradle文件中配置对应的依赖。
//根目录 build.gradle文件
buildscript {
dependencies {
classpath "com.alibaba:arouter-register:1.0.2"
}
}
接下来,继续在对应的组件中配置gradle依赖。
//组件 build.gradle文件
apply plugin: 'com.alibaba.arouter'
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments += [AROUTER_MODULE_NAME: project.getName()]
}
}
}
dependencies {
implementation 'com.alibaba:arouter-api:1.5.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.5.1'
}
配置完依赖以后,就可以开始使用ARouter框架的功能了。
对于依赖注入,我们首先需要让接口继成IProvider接口。
接下来在IAccountState接口实现处增加Route注解标记。
@Route(path = "/accountFeature/IAccountState", name = "IAccountState")
public class AccountStateImpl implements IAccountState {
@Override
public boolean isLogin() {
return AccountController.isLogin;
}
@Override
public String getUsername() {
return AccountController.currentAccountInfo.username;
}
@Override
public void init(Context context) {
}
}
最后就可以在使用IAccountState接口的地方使用AutoWired注解进行注入,请你注意,还需要在类初始化时调用ARouter的Inject方法。
public class FileFragment extends Fragment {
@Autowired
IAccountState iAccountState;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注意需要在页面初始化时调用inject方法
ARouter.getInstance().inject(this)
}
}
对比之前的反射,现在我们通过一个AutoWired注解以及一行inject方法就可以完成对应的实现,是不是更简单了呢?
那么AutoWired注解和inject方法到底干了什么事?为什么说ARouter的注入是动态注入呢?
想解答这些问题,我们还需要了解一下ARouter框架注入的实现原理。
首先当接口被标记了AutoWired注解以后,在编译期间,ARouter框架会通过注解生成器生成一个对应的注入实现类。
当调用ARouter的Inject方法时,实际是调用编辑器生成的 XXX$$Arouter$$Autowired的inject方法,然后将navigation(XXX.class)查询到的方法赋值给标记了Autowored注解的变量,完成注入的工作。这一步ARouter的注入看起来似乎采用的是静态注入,通过编译器生成代码来完成注入工作。
但我们继续查看navigation方法,看看如何将IAccountState的实现获取出来。通过一步一步查看navigation的调用,我们最后可以得到获取接口实现代码在LogisticsCenter类中的completion方法中,其中有一段关键的代码。
public synchronized static void completion(Postcard postcard) {
//... ...
Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
IProvider instance = Warehouse.providers.get(providerMeta);
if (null == instance) { // There's no instance of this provider
IProvider provider;
try {
provider = providerMeta.getConstructor().newInstance();
provider.init(mContext);
Warehouse.providers.put(providerMeta, provider);
instance = provider;
} catch (Exception e) {
throw new HandlerException("Init provider failed! " + e.getMessage());
}
}
postcard.setProvider(instance);
//... ...
}
通过代码可以看到,第8行代码是通过Class的getConstructor().newInstance()方法来获取接口的实例,其实也就是通过反射的形式。所以从本质上来看,ARouter的注入采用的是动态注入的方式。
至此,我们完成了Sharing项目的注入改造。虽然最终使用ARouter也是采用反射的机制,但是ARouter框架通过定义注解以及自动生成绑定代码的方式,大大减少了我们编写非业务模板代码的工作量。
路由
接下来我们来看路由的设计。同样以Sharing项目为例,我们来看下之前采用反射的形式绑定的代码。
与注入框架是同样的问题,当全局有几百个页面需要管理调整时,我们要维护这么多的页面反射代码就非常麻烦了,同样也会产生大量的非业务模板代码。
路由的设计思路也比较简单,就是通过建立路由映射表来统一管理页面的地址。
当查询对应的地址时,则返回对应跳转地址的实例。我们通过Sharing项目示例来看看ARouter具体的使用方式。
首先同样需要引入ARouter的配置,你可以参考前面注入介绍的配置方式。接下来就是在具体的页面上定义对应的路由地址。
配置完路由地址以后,就可以通过调用ARouter的navigation方法获取到对应的页面实例了。
那么前面说的路由表在哪里呢?与注入类似,ARouter同样在编译期间,通过注解生成器生成了对应映射关系,通过Map来进行保存。你可以在./build/generated/ap_generated_sources/debug/out/com/alibaba/android/arouter/routes目录下找到生成的类。
相信你已经发现了使用路由框架的好处,除了方便我们自动生成模版代码,还可以让我们更灵活地扩展功能。例如我们可以在服务端同步配置路由地址,从而动态控制页面的跳转。还可以自定义拦截器,在页面跳转之前自定义一些操作。关于ARouter更多的使用介绍,你可以参考官网的说明文档。
总结
今天我们一起学习了路由以及注入框架的设计思路、原理,并结合Sharing项目带你熟悉了这些框架如何使用。
框架给我们带来的好处是自动帮助我们生成模版代码,让我们可以更加专注在业务功能的实现上。同时使用框架可以提供统一的管理方案,让代码的维护更加简单、扩展更加灵活。
对于注入框架来说,常见的注入方式有静态注入以及动态注入。它们各有优缺点,静态注入性能好,能在编译期间进行检查,但是对于组件动态加载的支持不够友好;动态注入通常都是采用反射,所有有一定的性能损耗,但是又因为反射带来了灵活性,非常适合用在动态加载的场景之中。
对于路由框架来说,主要是建立关键的路由地址与跳转地址的映射表,借助路由地址来达到解耦的目的。当然除了这个优点以外,路由框架还支持拦截、降级等扩展功能,能让我们更加灵活地开发业务功能。
这里我给你推荐4个常用的注入及路由框架,你可以点击链接更深入了解框架的设计及使用。
至此,Sharing项目已经完成了路由以及注入框架的改造,在编译上也完成了组件的拆分以及彼此之间的依赖解耦。但是在运行时某些组件没加载时,如果没有兼容性的处理,依旧会有问题。
下一节课我们一起来探索组件运行时的耦合怎么来进行兼容处理,敬请期待。
思考题
感谢你学完了今天的内容,今天的思考题是这样的:框架带来的收益是明显的,但很多公司还会禁止团队使用开源的框架,你觉得是为什么呢?
欢迎你在留言区与我交流讨论,也欢迎你把它分享给你的同事或朋友,我们一起来高效、高质量交付软件!
- peter 👍(0) 💬(1)
请教老师几个问题: Q1:Java代码中,动态加载组件是怎么操作的?system_load,类似于这样的系统API调用吗? Q2:ARouter有坑吗?实际使用中是否有一些坑? Q3:热更新的官方方案,需要使用Google Play的API。目前不能使用,是因为墙的原因吗?
2023-03-08 - godliness 👍(0) 💬(0)
注入方式对比图中,优缺点是不是写反了,还是我理解的有误啊...
2023-10-26