组件化那些事

3,278 阅读11分钟

背景

我司之前一直采用MVP+Dagger2+Retrofit+Rxjava的项目结构。这种结构对于我们这种只有几个人的团队来说一直没有什么问题,因此使用了多年。直到18年初,公司决定扩展海外业务。我们海外的业务模式是这样的:

  1. 采用挤牙膏的运营方式,前期只会有国内的部分业务,后期会慢慢把国内的业务移植过去。
  2. 不同地区有不同的APP,这些APP可能有不同的业务功能。
  3. 海外的APP会有UI、逻辑等细微的不同。

在这样的背景下,我们决定实施组件化,从而实现组件的多APP复用。

项目结构

我们的项目结构经过了三次变更,最终它的样子是这样的。

我们将项目自上而下分为四层:壳工程、业务组件层、业务相关基础组件层、业务无关组件层。这四层分别承担的责任如下:

  • 壳工程:壳工程主要用来做一些打包的配置和一些对全局都有影响的功能。打包配置这个很好理解,例如:渠道包配置、风味配置等。对全局有影响的功能主要是一些检测APP运行状态的功能。例如:我们会在Debug模式下开启LeakCanary、会检测UI渲染是否失帧等。另外,我们集成了Tinker,也是在这个模块下完成的。
  • 业务组件:业务层的代码,每个组件单独出去都是一个完整的功能,它们还是沿用之前的MVP架构。
  • 业务相关基础组件:这一层是为业务层服务的。原则上这一层不要有UI的逻辑处理,只提供业务能力。UI部分可以放到业务组件中。但是,这个原则并不是一成不变的。例如,我们为扫码功能、分享、地图等功能都提供了完整的UI。
  • 业务无关组件:这一层就和业务完全无关了。

整个项目我们使用多仓库的管理方式。我们的仓库管理方式是这样的:

  • APP壳工程和每个业务组件都是一个单独仓库。
  • 业务相关基础组件整合到一个仓库。
  • 业务无关组件整合到一个仓库。

这样做的原因是:业务层的组件必然会在不同的APP中有所差异,我们的设计必须拥抱这种差异。 我们期望每个业务组件的改动都以版本的形式留下记录,这样我们可以尽可能的使组件在不同的APP间复用。因此我们需要每个业务组件是一个单独的仓库。但是,仓库过多,势必会造成仓库、版本等管理上的困扰。因此我们把业务相关组件层和业务无关组件层分别整合到一个仓库中。这样做的另外一个原因是我们对于业务相关基础组件和业务无关组件在不同APP的差异是拒绝的,我们希望不同的APP可以共用这些组件。

业务组件是没有办法单独运行的,它必须集成到壳工程中。这种集成有两种,一种是以aar的方式。另一种以module的方式。我们APP发版是通过一个APP管理系统来完成。在APP release打包时,我们可以选择需要的组件aar即可,因此在打release包时,是没有办法依赖本地module的。在开发阶段,所有的代码都在本地,我们可以通过配置debug.properties来指定依赖哪些module和aar。

组件间通信

说到组件间通信,目前已经有了很成熟的轮子,ARouter、WMRouter等。WMRouter开源较晚,我们做组件化时还没有出现。因此我们当时主要调研了ARouter,但是,最终放弃了。ARouter对于我们来说有点重量级了。因为它不仅提供了路由功能,还提供了参数注入、拦截等目前我们不需要的功能。如果我们要使用好它,项目改造较多。其次,它对内存有一定的消耗。它内部维护着一套路由表以及参数的对应表。这些都是常驻内存的。而我们一些东南亚国家的用户他们的手机非常差,内存是我们不得不考虑的一个因素。最终我们做了一个轻量级的接口+实现的通信框架。

每个业务组件对外提供通信服务都需要两部分完成:接口+实现。接口部分用来声明该业务组件可以提供哪些服务,它们会被下沉到业务相关基础组件的一个单独Module中,按包名区分。实现部分是具体的服务实现,它们放在各自的业务组件中。我们可以像这样使用它。

//注册
ServiceManager.getInstance().register(IMainService.class,new MainService());

//使用
ServiceManager.getInstance().get(IMainService.class).invoke();

除此之外,我们在开发时,只会让几个module参与编译。因此可能会出现ServiceManager.getInstance().get(IMainService.class)返回null的情况。为了不让程序崩溃,我们这里采用动态代理的方式进行了处理,通知开发者该组件没有注册。

组件初始化

在介绍组件间通信时,我故意忽略的一个细节,那就是接口和实现是在什么时候注入到路由表中去的。要说清楚这个问题,就不得不提组件入口。我们为每个组件提供了初始化能力。

public interface IApplicationLike {
    void onCreate(Application application);
}

每个组件都可以实现了IApplicationLike来初始化一些当前组件需要的功能。

public class MainApplication implements IApplicationLike {
    @Override
    public void onCreate(Application application) {
        ServiceManager.getInstance().register(IMainService.class,new MainService());
    }
}

除此之外,我们还在业务相关组件有一个GlobalApplicationLike,它用来初始化一些公共的功能。

public class GlobalApplicationLike implements IApplicationLike {
    static {
        //兼容21之前的svg。
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
    }
  
    @Override
    public void onCreate(Application application) {
        initServerApiUrl(getApplication());
        NetWorkUtil.initNetWork(getApplication(),UserCache.getInstance(getApplication()).getSessionId());
        initImageTool(getApplication());
        initShare(getApplication());
        initApplicationComponent(getApplication());
        initLogTools(getApplication());
        initPrintLogger();
        initPrintService(getApplication());
        initLanguage(getApplication());
        initStatisticTool(getApplication());
        initLoginOutService();
    }
}

这些实现IApplicationLike是通过字节码注入技术插入到Application.onCreate()中的。关于字节码技术,你可以看这篇文章Android编译期插桩,让程序自己写代码(三)

改进组件初始化

上套方案是我们最初的设计。它的缺点很明显那就是GlobalApplicationLike会依赖太多的module,而且一旦新增需要初始化的公共库那必须修改GlobalApplicationLike。直到我看到了知乎的组件化方案。它对初始化方案的描述是这样的:

有些组件有在应用启动时初始化服务的需求,而且很多服务还是有依赖关系的,最初我们为每个组件都添加了一个 init() 方法,但是并不能解决依赖顺序问题,需要每个组件都在 app 工程中按顺序添加初始化代码才能正常运行,这使得不熟悉整套组件业务的人很难建立起一个可以独立运行的组件 app。因此我们开发了一套多线程初始化框架,每个组件只要新建若干个启动 Task 类,并在 Task 中声明依赖关系即可。

我们根据这个思想,开发了我们自己的Task框架。我们去除了知乎方案中的多线程初始化功能,因为我们不知道怎么处理依赖和多线程间的关系。同时,我们也增加了根据process name初始化的功能。

@Task(name="PluginAStart",depend = {"PluginAfter"},process = {"processName"})
public class PluginAStartTask extends Task {
   
  	public PluginAStartTask(String name) {
        super(name);
    }

    @Override
    protected void run() {
    }
}

组件化遇到Dagger2

我在前面提到过,我们的项目用到了Dagger2。Dagger2有两种使用方式,一种是使用与Android平台无关的注入方式。

mainApplicationComponent = DaggerMainApplicationComponent.builder()
                 .baseApplicationComponent(getBaseApplicationComponent())
                 .mainApplicationModule(new MainApplicationModule(getApplication()))
                 .build();

这种方式比较灵活,与组件化并不冲突。具体的做法是我们可以在业务相关基础组件层创建一个BaseComponent用来提供公共有的注入对象。以后各个模块的Component都依赖BaseComponent。

还有一种方式是使用Dagger.Android,它的出现是为了解决上文中提到的模板代码问题。这种方式提高了Dagger的易用性,但是它与组件化不兼容。不幸的是,我们的项目恰好采用了这种方式。因此我们必须解决掉它。

我们最终确定方案如下:整个方案分为两部分,一部分是lib库。另一部分是gradle插件。lib库核心类有两个,DaggerApplicationLike和ApplicationAndroidInjector。DaggerApplicationLike是为了在组件中替代DaggerApplication,它的代码基本都是DaggerApplication的拷贝,就不贴出来了。ApplicationAndroidInjector实现了AndroidInjector,它是用来为Application提供注入的。ApplicationAndroidInjector中,有一个DaggerApplicationLike的集合。这个集合内的元素是通过gradle插件利用字节码注入技术在编译期注入的。在inject时,我们合并DaggerApplicationLike集合,生成全新的activityInjector、serviceInjector等注入给Application。

public class ApplicationAndroidInjector<T extends Application> implements AndroidInjector<T> {

    private final List<DaggerApplicationLike> mDaggerApplicationLikes = new ArrayList<>();

    public ApplicationAndroidInjector(){
      //在这里gradle插件会在编译期通过字节码注入技术将所有实现了DaggerApplicationLike的类注入到mDaggerApplicationLikes中
    }
  
    @Override
    public void inject(T instance) {
      //combineActivityInjector()用来合并mDaggerApplicationLikes中的activityInjector。生成一个全新的AndroidInjector注入到Application中去。
       	injectActivity(instance, DispatchingAndroidInjector_Factory.newDispatchingAndroidInjector(combineActivityInjector()));
        injected(instance);
    }
  
  protected void injectActivity(T instance, DispatchingAndroidInjector<Activity> dispatchingAndroidInjector) {
        if (instance instanceof DaggerApplication) {
            DaggerApplication application = (DaggerApplication) instance;
            DaggerApplication_MembersInjector.injectActivityInjector(application, dispatchingAndroidInjector);
        }
    }
}

ApplicationAndroidInjector使用如下:

public class TestApplication extends DaggerApplication {
 
    @Override
    protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
        return new ApplicationAndroidInjector<>();
    }
}

拆分过程

  • 规划。我个人把规划分为技术规划和产品规划。

    1. 技术规划主要是结合我们当前的项目做一些技术选型。我上面提到的项目结构、组件间通信、组件初始化等都属于技术规划。

    2. 产品规划是指我们要了解产品未来的发展方向以及近期可能的迭代需求。我们做设计时要有一定的前瞻性,了解产品未来的发展方向可以帮助我们拆分出更合理的业务组件。为什么这么说呢?我举个例子。现在很多企业都忙于变现,我们也不例外。我们为APP增加了会员功能,不过这个功能比较隐晦,它被放到了个人模块里。在做组件拆分时,很自然的我们把会员功能放到了个人模块里。过了一段时间,产品要大力拓展会员功能。会员功能在个人组件中的比重也越来越大,以至于我们不得不把会员功能从个人组件中拆分出来。假如在组件化拆分时,我们就了解到会员功能会被拓展,那么我们在最初就会把会员功能拆分成一个组件。

  • 具体实施。

    通常,组件化拆分是一个漫长的过程,中间往往会穿插着新功能的迭代。因此,在拆分过程中一定要保证项目的完整和正确,以便可以进行功能迭代。因此拆分步骤要遵循这个原则。首先我们应该先拆分出一个框架。这个框架是这样的,它有四层:app壳、业务层、业务相关基础层、业务无关基础层。这四层每层都是一个moudle,因此我们项目首先会被拆分成四个module。这四个module的拆分应该也是渐进式的,具体做法如下:

    1. 先从原项目拆分出app壳,这是很简单的。
    2. 从原项目中拆分业务相关基础层和业务无关基础层应该是按功能模块为单位的。例如,把网络作为一个整体移动到业务无关基础层、把支付移动到业务相关基础层等。这里分包的时候要按模块划分,方便我们拆分拆分组件。
    3. 每移动完一个功能,都要commit代码。

    这样我们整个组件化的架子就搭起来了,剩下的就是将各层拆分到不同的组件。业务相关基础层和业务无关基础层的拆分就很简单了,只需要按照不同的包结构拆分即可。我们的工作主要是将业务层拆分成不同的业务组件,在这个过程中,我们会下沉一些公用的类到业务相关基础组件中。

    最后,配置仓库。