关于当前公开的组件化方案存在的问题与解决方案探索

栏目: IOS · Android · 发布时间: 4年前

内容简介:经过长时间的摸索,目前打造出了全新的组件化方案,可以说已经有点摆脱组件化本身,而是做了较高的层次提升,做了大量的自动化工作。具体是:目前设计的整套方案,支持一键生成壳app,下一秒就做业务开发的境界。

经过长时间的摸索,目前打造出了全新的组件化方案,可以说已经有点摆脱组件化本身,而是做了较高的层次提升,做了大量的自动化工作。

具体是:

目前设计的整套方案,支持一键生成壳app,下一秒就做业务开发的境界。

同时自动支持组件化和插件化,并集成各种规范和检查。

让任何新手都可以像工作多年的架构师一般工作,不再因业务膨胀而让app出现各种可怕的耦合。

开发人员只需要去完成业务代码,不再关心任何开发的细节。

同时打通业务后台,监控,报表,git等,

让整个开发变得简洁而优雅。

基于对组件化和插件化于自己的工程做了实践和一些列的改进后。

想写点系列文章,记录下就目前公开的 组件化方案 和 插件库 存在的问

题, 和这些方案就组件化过程没提到的一些实际问题,

结合自己在项目做出的改进方案做些记录,分享。

这个估计会是一个相对比较长的系列,打算先各写4篇。

先从组件化开始,毕竟先对来说,插件化深度更高,

从浅到深,符合认知,也方便我自己耐心的写下去。

起航

1. 四处乱跑的AppRuntime

关于当前公开的组件化方案存在的问题与解决方案探索

大部分公开的方案使用接口的方式的,都是有个第三者(AppRuntime),各个基础库的服务往他注册或者通过它来调用服务。

注册是用上帝模块(如APP模块,其依赖所有模块)来统一向基础的第三方模块AppRuntime注册服务.

然后业务再通过AppRuntime的getSercvie()获取服务。

举个栗子

具体如下面这样的:

关于当前公开的组件化方案存在的问题与解决方案探索

所以在项目代码中会有大量的类似下面的代码:

AppRuntime.getService(xxxInterface.class).doSomeThing();

获取服务前,当然还是需要注册服务,所以在app模块的初始化时候,会有类似下面的大量注册服务逻辑

AppRuntime.registerService(xxxInterface.class,xxxImpl.class);
AppRuntime.registerService(xxxInterface.class,xxxImpl.class);
AppRuntime.registerService(xxxInterface.class,xxxImpl.class);

然后这些服务一般都会有个基础的服务类,类似下面这样,从而好调用初始化逻辑。

public interface IService {

    void onLoad(Context context, Config config);

    void onUnload();
}

简单理解为,这个把以前我们在Application的onCreate()时候做的一堆类似初始化xxx的逻辑,下放到了各个xxxService里面去了。

例如我们常用的Bugly初始化,我们初始化时候会让传账号的id,从而让我们好跟踪某个crash是谁的,

因此我们调用Login的服务获取uid,这就让Bulgy需要在login后面初始化,才能保证正确得到uid。

public class BuglySercie{

    void onLoad(Context context, Config config){

        int uid= getService(Login.class).getUid;

        setUid(uid)
    }

}

//然后在Login服务初始化时候,可能会打log,然后我们的log是用如slf4j的库等来做的
public class Login{

    void onLoad(Context context, Config config){

        Logger logger= LoggerFatory.getLogger(Login.class);

        logger.info("xxxx);
    }

}

通过往共同依赖的第三方模块注入服务后,模块间可以达到良好的解耦合,面向接口编程。

不直接引用具体的实现。

但上面的方式有两个大的问题没解决好。

  • Service间初始化顺序问题。

    这个我想看完上面示例代码,应该很快就意思到这个问题了。

    服务相互依赖后,必然存在初始化顺序问题,假设A依赖B的Service业务能力,那么在初始化A前,需要先往AppRuntime注册B,从而保证A初始化时,能正确的拿到B。

    一开始可以通过手工的在注册时候显式的排顺序去做,但随着注册的服务从几个,涨到几十个后,其复杂的依赖关系已经无人能知晓,后续再增加服务,插到其中, 完全有可能导致循环依赖情况,并且很难被及时发现并解决。

  • 增删业务不便。

    做组件化,经常聊的一个问题就是 组件单独调试和联调等问题。

    单独的Application启动器来做组件单独调试:

    在平时的开发过程,开发人员只想对自己的新功能做测试,所以会想只单独注册注册的Service调试,并不像现在这样一次性注册所有,然后把所有业务代码都编译进去,从而拖慢编译速度。按照这方案 独立的application来启动或者别的联调方案也好,你都不可避免要把这些初始化语句做个 增删 ,从而保留只需要的那么几个服务。

    重要的是随着业务膨胀,后期完全可能存在做极速版app或者做插件app的问题,这时候也需要对业务做裁剪,保留功能,从而缩包。

    只要app一做大,注册的服务一多达到几十个,目前我们的业务已经膨胀到近30个了,那以后完全是开发不友好的。

    灵活性欠佳,我们希望开发同学不要再去关注这样的细节了。

业界的解决方案

  1. ArmsComponent ,具体可以看 这篇文章 ,在 2.4 组件的生命周期 有提到问题和思考解决方案,不过采用最简单的方式,另外两种觉得技术难度大,收益比不高,所以也没有解决.

    现有的解决方案大概有三种:

    1. 在基础层中提供一个用于管理组件生命周期的管理类, 每个组件都手动将自己的生命周期实现类注册进这个管理类, 在集成调试时, 宿主在自己的 Application 对应生命周期方法中通过管理类去遍历调用注册的所有生命周期实现类即可

      1. 使用 AnnotationProcessor 解析注解在编译期间生成源代码自动注册所有组件的生命周期实现类, 然后宿主再在对应的生命周期方法中去调用
    1. 使用 Javassist 在编译时动态修改 class 文件, 直接在宿主的对应生命周期方法中插入每个组件的生命周期逻辑

      我最后还是选择了第一种方法, 因为后面两种方法虽然使用简单, 还可以自动化的完成所有操作, 非常炫酷, 但是这两种方法技术实现复杂, 在不同的 Gradle 版本中还会出现兼容性问题影响整个项目的开发进度, 较难维护, 还会增加编译时间

  • 得到app开源的组件化库 ,github有2765个Star的项目,其模块间的数据交互方案和上面的一样,没很好的解决问题。
  • 聚美组件化 ,和上面提到的方案一样,也没很好的解决问题。

  • JIMU ,大同小异,也没彻底解决好问题。

  • 微信
    对依赖等问题做了很好处理。具体可看这篇文字 微信Android模块化架构重构实践 。不过看起来还是得手动注册服务的感觉。
    且不得不说,如果微信每次启动都需要走一遍这个流程,做初始化的配置,生成一张依赖关系树状图,拓扑排序,然后执行。
    类似的启动初始化逻辑,估计也会导致了目前每次微信启动速度变得越来越慢。重要的是这些都变成了运行时的了,没办法在编译时提前暴露问题。

解决方案探索

我们希望有更好的方案,能否满足一下几点:

  1. 自动注册服务。类似registerService()语句很冗余,能否省去,程序自动知道我这个类就是服务,然后自动帮我写注册,从而也间接解决前面提到的 问题2,增删业务不便

  2. 编译时确定注册顺序逻辑。 我们想编译时就计算出整体的依赖关系,从而编译时候就清楚 整个依赖关系,然后生成有向无环图,接着自动根据依赖关系 排序 初始化,省去人工排序。

  3. 编译时输出服务依赖注册关系。随着业务膨胀,没人能完全知道整个服务的初始化顺序,有时候定位问题需要知道他们之间的关系,所以我们想能否输出类似依赖关系树图的东西,让开发人员能够看到整体的服务关系。类似于Aroute也可以打印路由关系一样。

  4. 服务自动注入能力。不再需要手动的去写那堆 AppRuntime.getService(xxxInterface.class) 的冗余逻辑。做到像Dagger一样的,自动给我注入。

    public class MainActivity extends AppCompatActivity {
    
        @Inject
        IPlayService playService;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            playService.playSomeThing(url);
        }
    }
  5. 进程控制。

    经常会有些业务逻辑能力是跑在另外的某个进程的,例如有些会把web的能力单独弄成一个进程,保证crash了也不会影响别的经常,

    或者把下载服务能力等弄成一个单独的进程。为此类似的,我们需要把注册服务在进程上做区分。

解决方案:

在经过长时间的尝试之后,终于探索出了一个全新的解决方案,且满足以上的所有想法,同时性能更优的解决方案。

这个方案不需要用很深的黑科技,但确实是一个新的突破,属于相对综合的更优方案。

  • 首先,为了达到服务自动发现和注册,可以用注解来做。即在服务类添加定制的注解,从而做到编译时依赖关系检测,注入,排序等操作。

  • 接着,如果依赖关系是DAG即有向无环图,那无环,用拓扑 排序算法 将图变成一个数组,从而获得正确注册顺序做注册。

  • 最后,利用这个正确初始化数组,去自动生成注册代码,避免我们手工注册服务。同时输出这结果给开发人员看。

具体解决方案

整体流程图:

关于当前公开的组件化方案存在的问题与解决方案探索
  1. 生成注解配置。我们在业务写好对应的服务注解等后,在编译时候通过 AnnotationProcessor 注解处理器,会生成一个对应的依赖关系文件 FALCO_CONFIG关于为何要做这步,后面会做解释,因为这涉及几个复杂的问题,是纯用注解没办法绕过的

  2. 生成注册服务类和自动注入服务。 接着FalcoPlugin,一个gradle的插件,会在执行JavaC前解析这个文件,做生成依赖树图,如果是DAG就拓扑排序然后生成一个正确的依赖关系数组,接着将这个数组生成一个 AppAutoRegisterService.class 文件到项目代码中去,这部分就是parseFileToDag()->TopoSort()->wirteResultToJavaFile()做的事情。

    与此同时,会把代码中加了@AutoInject注解的变量,自动插入赋值语句。

    这么做的好处就是可以省去xxx.bind(this)操作。你看ButterKnife,Arouter等框架,就存在大量的类似的xxx.bind(this)的操作,背后是为了执行对应的赋值操作,现在可以省了。此处应有掌声。

  3. 加载服务LoadAutoRegiser()。启动时加载这个 AppAutoRegisterService.class 生成的类,好完成我们的注册服务。

通过这个流程,我们达到了期待的效果,很开心。

对于上面的方案想法,相对业界目前公开的方案来说,相对更好,做到了编译时处理,自动注入服务,同时不影响编译时性能,把前面提到的1-5个问题得到解决。

一些细节的解释

1. 关于注解的说明

@AutoRegisterService(value = {IFeedReplyUIService.class},
    dependsClass = {IUserInfoService.class, IRelationService.class, ITopicService.class},
    processName = {PROCESS.WEB})
public class FeedReplyUIService extends FeedReplyService implements IFeedReplyUIService {

    @AutoInject
    ILogService iLogService;
}

在项目代码中,需要手动写一个 @AutoRegisterService 注解,同时声明自己实现的服务在value=xxx,如果你依赖了某个服务,需要在dependsClass={}写上依赖类,对于需要跑在特定Process的需要加多processName = {}。

这里说明下为何还需要手动的去声明下依赖的服务类,而不是自动发现依赖关系。**原因很简单,性价比低,而且很难做到。**

假设A类需要cClass的服务先初始化,然后背后的调用链条是这样的:

在初始化A时候,在初始化代码时候,可能调用bClass.init(),然后在这个init()里面,可能调用了cClass.doSomeThing()。类似如此的嵌套后,很难简单快速的得到A初始化需要的是谁。特别是在如果还存在运行时的判断条件后,那就更不好做判断了。

        if(debug){
            cClass.doSomeThing()
        }else{
            dClass.doSomeThing()
        }

因此对于依赖关系我们最后选择叫给了开发同学,自己去做这件事情。

2. 生成FALCO_CONFIG文件

为何要去利用注解来生成这个配置文件呢?

这是个蛮复杂的问题的,涉及到较多的Gradle,对性能和效率优化的考虑。

为便于说明,先说下目录结构

对应目录结构如下
App
    -AppAutoRegisterService.class
aModule
    -IFirstService.java
    -FirstServiceImpl.java
bModule
    -ISecondService.java
    -SecondServiceImpl.java
cModule
    -IThirdService.java
    -ThirdServiceImpl.java

在经过编译后,每个module对应的注册服务会统一汇总到这个类文件总来,运行时反射调用run方法,从而完成自动注册服务。

public class AppAutoRegisterService extends AppBootloader {

      @Override
      public void run(Application application) {
        super.run(application);
        AppRuntime.registerService( "aModule.IFirstService","xxx.FirstServiceImpl",false,new String[]{"main","tools"});
        AppRuntime.registerService( "bModule.ISecondService","xxx.SecondServiceImpl",false,new String[]{"tools"});
        AppRuntime.registerService( "cModule.IThirsService","xxx.ThirdServiceImpl",false,new String[]{"main"});
        loadService();
      }

}
  • 如果纯用注解处理器来做,那么我们确实可以去生成类似上面这样的文件,完全没必要配套多Gradle插件,那么为何还要这么做呢?

    这就要从上面的目录结构说起,假如有一天,在正常开发时候,我删除了cModule,那么理论上我应该把对应的注册语句也剔除掉,否则会报错。

    但这个删除模块的操作,对于注解处理来说,是无法感知到的,只有Gradle他知道当前参与编译的目录结构,因为在Setting.gradle写着啊。

    这就说明白了为何不能纯用注解处理器来做了。

  • 那为何不纯用 Gradle + Javassist 来做,在编译时读取到类文件有这个注解时候,自动解析处理,然后生成这个代码呢?

    因为这个性能差啊,想必做过类似Transform功能的人都知道,遍历整个项目成千上万个类文件,而处理添加注解的几个文件,无异于大海捞针,随着项目膨胀,这个时间就上去了,所以万万不可。开头在现有解决方案 ArmsComponent 那里也说了,这个是会拖慢编译时间的。

所以基于性能和能够增删注入服务,最终采用了难度最大的 AnnotationProcessor + Gralde Plugin 联合方案。

前者由于是语言天然支持的,能自动把包含注解的信息作为输入源,非常高效。

后者Gradle作为打包Apk的利器,功能强大,可以说是能为所欲为。

将两者的优势结合起来,那是相当的妙。

关于当前公开的组件化方案存在的问题与解决方案探索

最终造就了这个方案:

在注解处理器,我会把注解带的信息等统一汇总到一个文件,接着到编译时利用Falco这个自研的Gralde插件,对这个配置文件做解析,完成建树,检测,拓扑排序,最后利用JavaPoet生成类文件插入到项目中去这样一套流程。

性能

由于只是解析文件生成类文件等过程,经过测试,整个过程可控制在5S以内,而且不受项目膨胀而拖慢编译性能,可以说是目前看到的相对优秀的方案。

最后,对于 @AutoInject 这个注解也一样,也是针对配置文件说的特定类做处理,不遍历整个项目来做。

3. 服务的漏注依赖关系问题

在前面的 解决方案探索 时候,提到了编译时确定依赖关系,但不得不说在,这个关系只能靠开发人员来告诉我,所以,这就存在了服务的漏注依赖关系问题。

这是一个现实的问题,本来我们的服务是有依赖某个服务的,但它并不申明,但实际代码中就是调用了,这个需要做兜底逻辑。

这个没办法通过代码的应用关系等来做判断,因此只能靠开发自动手动注册和检测逻辑来做。

为什么说不能检测出来,是因为在实际开发过程中发现,完全可能在相对复杂的调用链路中,导致在调用链路的某个地方,某同学加上对C服务的调用,然后没在注解上显示的去声明多这个依赖的情况。

为何这么说了,因为在用这个方案的实践中,有时会 偶现依赖关系顺序的crash问题

最后跟下来就是因为在 A中的某个初始化的调用链路中加多对C的调用,然后并没有在A中显示的声明对C的调用,这导致了两者在排序的时候,有时是A在前面,有时是C在前面先被初始化。为何会出现这个排序前后的跳变,而不是固定的关系呢,因为他们是通过 注解处理器 处理后,往 FALCO_CONFIG 文件插入注解的信息的,然后再作为输入源给图做排序,因此也就存在先后了。

而且因为支持增量编译,所以存在着修改 或者 删除某个Module,那么只针对这个模块做增量构建,然后插入或者 删除某个module对应依赖关系的配置 到 FALCO_CONFIG 里面去。

解决方案:

为了处理好这个问题,最后做多了运行时的依赖关系检测逻辑,如果存在循环依赖,或者依赖了别的服务,但是没有显示的声明的话,那就做报错和弹Toast提醒。经过一段时间的迭代后,整体对应的问题都能被提前发现了。

小结

我认为通过上面的方案,目前解决了开头提出的两个顺序+增删不便问题。

对应的几个子问题为

  1. 服务手动注册
  2. 服务手动注入
  3. 依赖关系显示声明,编译时检测
  4. 做业务的单独调试 和 联调时候 更方便,不再需要拼凑注册逻辑。

以上是针对于服的注册,调用与注入的一个讨论,篇幅比较长了将近一万字了,就先停笔,后面会就组件化另外的问题再做讨论。

有任何问题想法也欢迎留言沟通,辛苦你认真看完这么长的文章了,希望对你有用。


以上所述就是小编给大家介绍的《关于当前公开的组件化方案存在的问题与解决方案探索》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

用数据讲故事

用数据讲故事

[美] Cole Nussbaumer Knaflic / 陆 昊、吴梦颖 / 人民邮电出版社 / 2017-8 / 59.00元

本书通过大量案例研究介绍数据可视化的基础知识,以及如何利用数据创造出吸引人的、信息量大的、有说服力的故事,进而达到有效沟通的目的。具体内容包括:如何充分理解上下文,如何选择合适的图表,如何消除杂乱,如何聚焦受众的视线,如何像设计师一样思考,以及如何用数据讲故事。一起来看看 《用数据讲故事》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换