跳转至

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接口。

public interface IAccountState extends IProvider {
    boolean isLogin();
    String getUsername();
}

接下来在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的配置,你可以参考前面注入介绍的配置方式。接下来就是在具体的页面上定义对应的路由地址。

@Route(path = "/fileFeature/file")
public class FileFragment extends Fragment {
}

配置完路由地址以后,就可以通过调用ARouter的navigation方法获取到对应的页面实例了。

fragments.add((Fragment) ARouter.getInstance().build("/fileFeature/file").navigation());

那么前面说的路由表在哪里呢?与注入类似,ARouter同样在编译期间,通过注解生成器生成了对应映射关系,通过Map来进行保存。你可以在./build/generated/ap_generated_sources/debug/out/com/alibaba/android/arouter/routes目录下找到生成的类。

相信你已经发现了使用路由框架的好处,除了方便我们自动生成模版代码,还可以让我们更灵活地扩展功能。例如我们可以在服务端同步配置路由地址,从而动态控制页面的跳转。还可以自定义拦截器,在页面跳转之前自定义一些操作。关于ARouter更多的使用介绍,你可以参考官网的说明文档

总结

今天我们一起学习了路由以及注入框架的设计思路、原理,并结合Sharing项目带你熟悉了这些框架如何使用。

框架给我们带来的好处是自动帮助我们生成模版代码,让我们可以更加专注在业务功能的实现上。同时使用框架可以提供统一的管理方案,让代码的维护更加简单、扩展更加灵活。

对于注入框架来说,常见的注入方式有静态注入以及动态注入。它们各有优缺点,静态注入性能好,能在编译期间进行检查,但是对于组件动态加载的支持不够友好;动态注入通常都是采用反射,所有有一定的性能损耗,但是又因为反射带来了灵活性,非常适合用在动态加载的场景之中。

对于路由框架来说,主要是建立关键的路由地址与跳转地址的映射表,借助路由地址来达到解耦的目的。当然除了这个优点以外,路由框架还支持拦截、降级等扩展功能,能让我们更加灵活地开发业务功能。

这里我给你推荐4个常用的注入及路由框架,你可以点击链接更深入了解框架的设计及使用。

至此,Sharing项目已经完成了路由以及注入框架的改造,在编译上也完成了组件的拆分以及彼此之间的依赖解耦。但是在运行时某些组件没加载时,如果没有兼容性的处理,依旧会有问题。

下一节课我们一起来探索组件运行时的耦合怎么来进行兼容处理,敬请期待。

思考题

感谢你学完了今天的内容,今天的思考题是这样的:框架带来的收益是明显的,但很多公司还会禁止团队使用开源的框架,你觉得是为什么呢?

欢迎你在留言区与我交流讨论,也欢迎你把它分享给你的同事或朋友,我们一起来高效、高质量交付软件!

精选留言(2)
  • peter 👍(0) 💬(1)

    请教老师几个问题: Q1:Java代码中,动态加载组件是怎么操作的?system_load,类似于这样的系统API调用吗? Q2:ARouter有坑吗?实际使用中是否有一些坑? Q3:热更新的官方方案,需要使用Google Play的API。目前不能使用,是因为墙的原因吗?

    2023-03-08

  • godliness 👍(0) 💬(0)

    注入方式对比图中,优缺点是不是写反了,还是我理解的有误啊...

    2023-10-26