likes
comments
collection
share

关于Android组件化的深度分析篇(四)大厂架构

作者站长头像
站长
· 阅读数 24

前言

忽如一夜冬风来🥱这天气刚好碰到周五,适合在家里冬眠八。

昨天分析了WeChat APP和智行APP的架构演化,今天再来分析一下其他的。今天周五了,只续一个哈。

关注公众号:Android苦做舟 解锁 《Android十二大板块PDF》 音视频大合集,从初中高到面试应有尽有;让学习更贴近未来实战。已形成PDF版

十二个模块PDF内容如下

1.2022最新Android11位大厂面试专题,128道附答案 2.音视频大合集,从初中高到面试应有尽有 3.Android车载应用大合集,从零开始一起学 4.性能优化大合集,告别优化烦恼 5.Framework大合集,从里到外分析的明明白白 6.Flutter大合集,进阶Flutter高级工程师 7.compose大合集,拥抱新技术 8.Jetpack大合集,全家桶一次吃个够 9.架构大合集,轻松应对工作需求 10.Android基础篇大合集,根基稳固高楼平地起 11.Flutter番外篇:Flutter面试+项目实战+电子书 12.大厂高级Android组件化强化实战

整理不易,关注一下吧。开始进入正题,ღ( ´・ᴗ・` ) 🤔

一丶从得到APP看组件化架构实践

  • 提到了组件生命周期、服务注册的实现
  • 提到了公共层定义组件服务、base层定义通用资源
  • 提到了 implementation 与 runtimeOnly 的代码 / 资源隔离效果;
  • 提到了 JIMU 插件的调试切换、智能配置功能;
  • 提到了 2 种调用组件声明周期的方法: javassist 和反射;
  • 提到了有序初始化组件的解决方案:StartUp、DAU

关于Android组件化的深度分析篇(四)大厂架构

1.Android彻底组件化Demo发布

1.1.JIMU使用指南

首先我们看一下demo的代码结构,然后根据这个结构图再次从单独调试(发布)、组件交互、UI跳转、集成调试、代码边界和生命周期等六个方面深入分析,之所以说“再次”,是因为上一篇文章我们已经讲了这六个方面的原理,这篇文章更侧重其具体实现。

关于Android组件化的深度分析篇(四)大厂架构 代码中的各个module基本和图中对应,从上到下依次是

  • app是主项目,负责集成众多组件,控制组件的生命周期
  • reader和share是我们拆分的两个组件
  • componentservice中定义了所有的组件提供的服务
  • basicres定义了全局通用的theme和color等公共资源
  • basiclib中是公共的基础库,一些第三方的库(okhttp等)也统一交给basiclib来引入

图中没有体现的module有两个,一个是componentlib,这个是我们组件化的基础库,像Router/UIRouter等都定义在这里;另一个是build-gradle,这个是我们组件化编译的gradle插件,也是整个组件化方案的核心。

我们在demo中要实现的场景是:主项目app集成reader和share两个组件,其中reader提供一个读书的fragment给app调用(组件交互),share提供一个activity来给reader来调用(UI跳转)。主项目app可以动态的添加和卸载share组件(生命周期)。而集成调试和代码边界是通过build-gradle插件来实现的。

单独调试和发布

单独调试的配置与上篇文章基本一致,通过在组件工程下的gradle.properties文件中设置一个isRunAlone的变量来区分不同的场景,唯一的不同点是在组件的build.gradle中不需要写下面的样板代码:

if(isRunAlone.toBoolean()) {
   apply plugin: 'com.android.application'
}else{
   apply plugin: 'com.android.library'
}

而只需要引入一个插件com.dd.comgradle(源码就在build-gradle),在这个插件中会自动判断apply com.android.library还是com.android.application。实际上这个插件还能做更“智能”的事情,这个在集成调试章节中会详细阐述。

单独调试所必须的AndroidManifest.xml、application、入口activity等类定义在src/main/runalone下面,这个比较 简单就不赘述了。

如果组件开发并测试完成,需要发布一个release版本的aar文件到中央仓库,只需要把isRunAlone修改为false,然后运行module:assembleRelease命令就可以了。这里简单起见没有进行版本管理,大家如果需要自己加上就好了。

值得注意的是,发布组件是唯一需要修改isRunAlone=false的情况,即使后面将组件集成到app中,也不需要修改isRunAlone的值,既保持isRunAlone=true即可。所以实际上在Androidstudio中,是可以看到三个application工程的,随便点击一个都是可以独立运行的,并且可以根据配置引入其他需要依赖的组件。这背后的工作都由com.dd.comgradle插件来默默完成。

关于Android组件化的深度分析篇(四)大厂架构

组件交互

在这里组件的交互专指组件之间的数据传输,在我们的方案中使用的是接口+实现的方式,组件之间完全面向接口编程。

在demo中我们让reader提供一个fragment给app使用来说明。首先reader组件在componentservice中定义自己的服务

public interface ReadBookService {
    Fragment getReadBookFragment();
}

然后在自己的组件工程中,提供具体的实现类ReadBookServiceImpl:

public class ReadBookServiceImpl implements ReadBookService {
    @Override
    public Fragment getReadBookFragment() {
        return new ReaderFragment();
    }
}

提供了具体的实现类之后,需要在组件加载的时候把实现类注册到Router中,具体的代码在ReaderAppLike中,ReaderAppLike相当于组件的application类,这里定义了onCreate和onStop两个生命周期方法,对应组件的加载和卸载。

public class ReaderAppLike implements IApplicationLike {
    Router router = Router.getInstance();
    @Override
    public void onCreate() { 
        router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
    }
    @Override
    public void onStop() {
        router.removeService(ReadBookService.class.getSimpleName());
    }
}

在app中如何使用如reader组件提供的ReaderFragment呢?注意此处app是看不到组件的任何实现类的,它只能看到componentservice中定义的ReadBookService,所以只能面向ReadBookService来编程。具体的实例代码如下:

Router router = Router.getInstance();
if (router.getService(ReadBookService.class.getSimpleName()) != null) {
    ReadBookService service = (ReadBookService)
router.getService(ReadBookService.class.getSimpleName());
    fragment = service.getReadBookFragment();
    ft = getSupportFragmentManager().beginTransaction();
    ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();
}

这里需要注意的是由于组件是可以动态加载和卸载的,因此在使用ReadBookService的需要进行判空处理。我们看到数据的传输是通过一个中央路由Router来实现的,这个Router的实现其实很简单,其本质就是一个HashMap,具体代码大家参见源码。

通过上面几个步骤就可以轻松实现组件之间的交互,由于是面向接口,所以组件之间是完全解耦的。至于如何让组件之间在编译阶段不不可见,是通过上文所说的com.dd.comgradle实现的,这个在第一篇文章中已经讲到,后面会贴出具体的代码。

UI跳转

页面(activity)的跳转也是通过一个中央路由UIRouter来实现,不同的是这里增加了一个优先级的概念。具体的实现就不在这里赘述了,代码还是很清晰的。

集成调试

集成调试可以认为由app或者其他组件充当host的角色,引入其他相关的组件一起参与编译,从而测试整个交互流程。在demo中app和reader都可以充当host的角色。在这里我们以app为例。

首先我们需要在根项目的gradle.properties中增加一个变量mainmodulename,其值就是工程中的主项目,这里是app。设置为mainmodulename的module,其isRunAlone永远是true。

然后在app项目的gradle.properties文件中增加两个变量:

debugComponent=readercomponent,com.mrzhang.share:sharecomponent
compileComponent=readercomponent,sharecomponent

其中debugComponent是运行debug的时候引入的组件,compileComponent是release模式下引入的组件。我们可以看到debugComponent引入的两个组件写法是不同的,这是因为组件引入支持两种语法,module或者modulePackage:module,前者直接引用module工程,后者使用componentrelease中已经发布的aar。

注意在集成调试中,要引入的reader和share组件是不需要把自己的isRunAlone修改为false的。我们知道一个application工程是不能直接引用(compile)另一个application工程的,所以如果app和组件都是isRunAlone=true的话在正常情况下是编译不过的。秘密就在于com.dd.comgradle会自动识别当前要调试的具体是哪个组件,然后把其他组件默默的修改为library工程,这个修改只在当次编译生效。

如何判断当前要运行的是app还是哪个组件呢?这个是通过task来判断的,判断的规则如下:

  • assembleRelease → app
  • app:assembleRelease或者 :app:assembleRelease → app
  • sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→sharecomponent

上面的内容要实现的目的就是每个组件可以直接在Androidstudio中run,也可以使用命令进行打包,这期间不需要修改任何配置,却可以自动引入依赖的组件。这在开发中可以极大加快工作效率。

代码边界

至于依赖的组件是如何集成到host中的,其本质还是直接使用compile project(...)或者compile modulePackage:module@aar。那么为啥不直接在build.gradle中直接引入呢,而要经过com.dd.comgradle这个插件来进行诸多复杂的操作?原因在第一篇文章中也讲到了,那就是组件之间的完全隔离,也可以称之为代码边界。如果我们直接compile组件,那么组件的所有实现类就完全暴露出来了,使用方就可以直接引入实现类来编程,从而绕过了面向接口编程的约束。这样就完全失去了解耦的效果了,可谓前功尽弃。

那么如何解决这个问题呢?我们的解决方式还是从分析task入手,只有在assemble任务的时候才进行compile引入。这样在代码的开发期间,组件是完全不可见的,因此就杜绝了犯错误的机会。具体的代码如下:

/**
 * 自动添加依赖,只在运行assemble任务的才会添加依赖,因此在开发期间组件之间是完全感知不到的,这是做到完全隔离
的关键
 * 支持两种语法:module或者modulePackage:module,前者之间引用module工程,后者使用componentrelease中已经发布的aar
 * @param assembleTask
 * @param project
 */
private void compileComponents(AssembleTask assembleTask, Project project) {
    String components;
    if (assembleTask.isDebug) {
        components = (String) project.properties.get("debugComponent")
    } else {
       components = (String) project.properties.get("compileComponent")
    }
    if (components == null || components.length() == 0) {
        return;
    }
    String[] compileComponents = components.split(",")
    if (compileComponents == null || compileComponents.length == 0) {
        return;
    }
    for (String str : compileComponents) {
         if (str.contains(":")) {
             File file = project.file("../componentrelease/" + str.split(":")[1] + "-release.aar")
             if (file.exists()) {
                 project.dependencies.add("compile", str + "-release@aar")
             } else {
                 throw new RuntimeException(str + " not found ! maybe you should generate a new one ")
             }
         } else {
             project.dependencies.add("compile", project.project(':' + str))
         }
    }
}

生命周期

在上一篇文章中我们就讲过,组件化和插件化的唯一区别是组件化不能动态的添加和修改组件,但是对于已经参与编译的组件是可以动态的加载和卸载的,甚至是降维的。

首先我们看组件的加载,使用章节5中的集成调试,可以在打包的时候把依赖的组件参与编译,此时你反编译apk的代码会看到各个组件的代码和资源都已经包含在包里面。但是由于每个组件的唯一入口ApplicationLike还没有执行oncreate()方法,所以组件并没有把自己的服务注册到中央路由,因此组件实际上是不可达的。

在什么时机加载组件以及如何加载组件?目前com.dd.comgradle提供了两种方式,字节码插入和反射调用。

  • 字节码插入模式是在dex生成之前,扫描所有的ApplicationLike类(其有一个共同的父类),然后通过javassist在主项目的Application.onCreate()中插入调用ApplicationLike.onCreate()的代码。这样就相当于每个组件在application启动的时候就加载起来了。
  • 反射调用的方式是手动在Application.onCreate()中或者在其他合适的时机手动通过反射的方式来调用ApplicationLike.onCreate()。之所以提供这种方式原因有两个:对代码进行扫描和插入会增加编译的时间,特别在debug的时候会影响效率,并且这种模式对Instant Run支持不好;另一个原因是可以更灵活的控制加载或者卸载时机。

这两种模式的配置是通过配置com.dd.comgradle的Extension来实现的,下面是字节码插入的模式下的配置格式,添加applicationName的目的是加快定位Application的速度。

combuild {
    applicationName = 'com.mrzhang.component.application.AppApplication'
    isRegisterCompoAuto = true
}

demo中也给出了通过反射来加载和卸载组件的实例,在APP的首页有两个按钮,一个是加载分享组件,另一个是卸载分享组件,在运行时可以任意的点击按钮从而加载或卸载组件,具体效果大家可以运行demo查看。

关于Android组件化的深度分析篇(四)大厂架构

1.2.组件化拆分感悟

在最近两个月的组件化拆分中,终于体会到了做到剥丝抽茧是多么艰难的事情。确定一个方案固然重要,更重要的是克服重重困难坚定的实施下去。在拆分中,组件化方案也不断的微调,到现在终于可以欣慰的说,这个方案是经历过考验的,第一它学习成本比较低,组内同事可以快速的入手,第二它效果明显,得到本来run一次需要8到10分钟时 间(不过后面换了顶配mac,速度提升了很多),现在单个组件可以做到1分钟左右。最主要的是代码结构清晰了很多,这位后期的并行开发和插件化奠定了坚实的基础。

总之,如果你面前也是一个庞大的工程,建议你使用该方案,以最小的代价尽快开始实施组件化。如果你现在负责的是一个开发初期的项目,代码量还不大,那么也建议尽快进行组件化的规划,不要给未来的自己增加徒劳的工作量。

2.Android彻底组件化-代码和资源隔离

2.1.代码隔离

在讲代码隔离之前,先大致看一下gradle3.0.0对添加依赖的语法变化。

首先compile被废弃了,而是分成了两个:implementation和api,其中api与之前的compile功能基本一致,不再赘述;implementation就比较高级了,其作用就是,使用implementation添加的依赖不会再编译期间被其他组件引用到,但在运行期间是完全可见的。这也是一种代码隔离。举个例子:

  • 组件A依赖lib1,既A implementation lib1
  • 组件B依赖组件A,既B api A

在gradle3.0.0之前,B是完全可以引用到lib1里面的类的,但是现在B在编译期间就做不到了,只能在运行期可以。这种思想有点类似于“下属的下属不是你的下属”的思想。但是这种隔离在组件之间是不起作用的,在上面的例子中A的所有类对B还是完全可见的,也就是没有做任何隔离的。不过implementation的确是一种有效减少编译时间的方式,还是上面的例子,lib1发生了变化,现在只需要编译A就可以了,而在之前B有可能也使用到了lib1,所以需要同时编译B和A。按照官方建议,大部分情况下都应该使用implementation来进行添加依赖。

此外还有两种变化,原来的apk语法被runtimeOnly取代,provided被compileOnly取代,其作用还是没变。上文也讲了,runtimeOnly有个极大的改动就是可以支持aar了,但是compileOnly还是只能支持jar!先做一个小结,目前gradle3.0.0的四种语法的功能和代码隔离效果见下图:

关于Android组件化的深度分析篇(四)大厂架构 从上图可以看出,在代码隔离效果上,runtimeOnly的效果是最好的!但是就可以直接使用了吗,答案是否定的。

2.2.资源隔离

在前面的文章中,一直在强调代码隔离,其实组件之间的完全隔离还有一层就是资源隔离,否则还是容易造成组件之间的耦合。这个在文章的“单独调试”章节中提到了一句,就是每个组件都需要指定一个资源前缀resourcePrefix,以避免集成后资源名冲突的问题。也就是说,一个彻底的组件化不仅要做到代码不能直接引用,资源也是不能引用的!但是runtimeOnly目前还做到资源隔离,我在JIMU的开源库上做了试验,app通过runtimeOnly引用sharecomponent组件,虽然sharecomponent的代码是不可见了,但是资源还是可以被app直接使用的并能成功运行。

从这一点上看,直接替换成runtimeOnly是不行的,为了达到这种效果,目前还是需要像JIMU一样,人为的加一层控制,所以从组件化方案的角度上看并没有变的更薄,不过幸好JIMU已经很简单了,有一定的gradle基础的人可以比较容易的理解。

2.3.调试切换

除了上面说的资源隔离导致不能直接用runtimeOnly之外,还有一个使用上的问题需要解决,这也是JIMU中compbuild插件提供的一个功能:自动切换单独调试和集成调试。在单独调试时,组件是一个application工程,其输出产物是apk文件,而在集成调试时,被依赖的组件是一个library工程,其输出产物是aar文件。对于runtimeOnly来说,对aar和jar是支持的,但是不能支持apk,所以如果想在单独调试和集成调试之间切换的话,需要人工修改runalone配置并修改build.gradle配置文件,然后还需要sync之后才能生效,这种修改是相当繁琐的。

在JIMU中,这个问题的解决是通过“智能”识别当前要调试的组件来解决的,对于要调试的组件将其设置为application工程,而将其依赖的其他组件默默修改为library工程,这种修改是即时生效的,对开发者是完全透明的。开发者直接点击AS的run功能区就可以随意的调试任意组件。AS的run功能区的图如下:

关于Android组件化的深度分析篇(四)大厂架构

2.4.总结

综上所述,我们对JIMU和gradle3.0.0做几点总结: (1)升级到gradle3.0.0之后,可以继续使用JIMU,不需要专门做兼容 (2)gradle3.0.0提供了implementation和runtimeOnly两种语法,它们都能实现一定程度的代码隔离效果,建议大家在今后优先使用 (3)implementation和runtimeOnly目前在资源隔离和调试切换上还不能满足组件化的要求,所以还是需要使用JIMU提供的完全隔离和随意切换功能

3.组件化:代码隔离也难不倒组件的按序初始化

3.1.前言

时至今日,Android项目中的组件化大家都已经非常熟悉了,但在各个细节方面还是有一些门门道道的内容,如果没有趁手的中间件支持,推行组件化的过程中还是会遇到阻碍。

3.2.问题的根源

这里我们再花一点时间来了解下问题的根源:组件化的基础是模块化,在做到模块化的同时,模块与模块在编写、编译期间也就达成了完全代码隔离,组件间的交互依靠 底层接口+服务发现(或者服务注册) 或者更加抽象为 “基于协议、隐藏实现”。这带来了编写、编译期间激增的代码耦合(注:此处语境遗漏,在达成编写、编译期间完全代码隔离的条件下,想要用比较原始的、直面问题的方式解决组件按序初始化问题,例如使用反射+无分支遗漏的逻辑涵盖所有组件组合情况,会导致耦合激增。)我知道这样说实在是太晦涩了,一点也不接地气,我们以一个简单的例子来配合说明。

interface IComponent {
    fun onCreate()
    fun onDestroy()
}

我们定义这样的接口来代表一个组件模型。案例设定为:一个宿主H+两个互无关联的组件A、B那么有:

class A : IComponent {
    override fun onCreate() {
       // A初始化逻辑
    }
    
    override fun onDestroy() {
    }
}
class B : IComponent {
    override fun onCreate() {
       // B初始化逻辑
    }

    override fun onDestroy() {
    }
}

另有

class H :Application {
    override fun onCreate() {
        A().onCreate()
        B().onCreate()
    }
}

我们以最简单的代码演示组件的加载和初始化环节。这里隐藏了一个问题:如果是手工编码,那么是存在代码边界的,编写、编译期间H无法直接访问A和B,我们只能通过反射去实现(否则编译不通过)。当然,也可以通过字节码技术实现

如果我们要让B先于A初始化,那么就调整其顺序,这对于手工编码方式而言,可能就是将编码变为:

XXX.loadComponent("Bpackage.B") //"Bpackage.B"为B的类路径
XXX.loadComponent("Apackage.A")

而利用字节码技术的,则需要增加排序功能或者读取全量配置功能。

案例2: 此时A组件依赖于B,必须等B组件初始化成功并得到结果后才能初始化。

思路1:先加载和初始化B,利用代码同步的特性,再初始化A

思路2:先加载和初始化B,修改组件模型,增加callback作为入参,异步初始化A

思路1存在很大的限制,比如其初始化需要参与网络通信或者数据库操作;思路2对于手工编码来说,会产生回调地狱,而对于字节码技术实现而言,就是一个噩梦

而且,JIMU已经投入使用挺长一段时间了,如果不是毫无选择,对于基类或者接口做无法版本兼容的操作都不应该被采纳

思路2的改进版:增加上下文,使得回调嵌套扁平化。

既然我们决定增加一个上下文,那么将初始化的管理工作进行封装就成了顺理成章的事情

为什么不使用官方StartUp而选择造轮子

在思考这个问题时,我们必须要清楚Startup的设计意图

Startup  |  Android 开发者  |  Android Developers

可在应用启动时简单、高效地初始化组件。

借助 App Startup 库,可在应用启动时简单、高效地初始化组件。库开发者和应用开发者都可以使用 App Startup来简化启动序列并显式设置初始化顺序。

我们知道,在Startup发布之前,各大SDK采用的初始化方式一般为两种:

  • 显式API调用,需要Application实例
  • 内部提供一个ContentProvider,并在其中获取Application实例。因为其特性,会在应用启动时被自动加载,而不再需要使用者显式的API调用

一般为了方便开发者,在manifest文件中写入SDK参数配置并利用Context(为了不造成泄漏,使用Application是最好的选择)读取配置的做法更受推荐。所以第二种方式的使用越来越多。

这就带来了一个问题:引入越多的SDK就会引入更多的ContentProvider,他们并不会随着初始化工作完成而消亡,而且加重了应用启动时AMS的负担。

业内存在一个著名的编程范式:约定优于配置,既然使用ContentProvider作为初始化入口已经被广泛接受,那么Google作为生态维护者提供一个官方库,使用统一的初始化入口,使用者只需要按照约定暴露初始化逻辑,并且提供了前置依赖使得任务可排序的功能。

到这里我们就可以明白这样几件事情:

  • StartUp中使用异步和其排序加载之间存在“矛盾”
  • StartUp不提供依赖有向无环图校验

因为StartUp更主要的是面向SDK,提供统一标准。SDK库之间出现“存在性上的先后关系”的场景本身就非常小,如果有“依赖”,SDK生产者在库内部都处理好了,一般也不会出现代码边界。

所以,Maat并不是一个和StartUp一较长短的功能库,而是为了解决特定问题而编写的功能库。这些问题又恰恰是StartUp所不涉及的

设计思路 相信大家对“同步”和“异步”都有比较深的理解,我们先提出三个参与初始化的角色:

  • 任务: 初始化工作的最小单元,清晰的知道自己的所依赖的任务,只有依赖的任务都执行完毕后才能执行,我们 以Task=Name[dependency1,dependency2,...] 来表示任务,例如 B[] ==> 无前置依赖的任务B, A[B]==> 任务A、依赖任务B
  • 任务集:所有任务的集合,可分析任务的所有前置依赖并判断是否存在循环依赖,对任务进行排序,记为TaskGroup={Task1,Task2,...}
  • 任务调度器:从任务集中取出任务派发执行的调度器

回顾我们最开始给出的例子,组件之前有存在性先后关系,必须要让依赖的组件完成初始化后才能开始加载。 那么任务调度器的工作方式是“同步”的,在“被依赖的任务”执行完毕前,依赖他的任务都必须阻塞等待。

但是思考一个问题:两个互相独立的任务,必须阻塞等待吗?答案显然,不是必须的。

这里举一些例子:

有任务集: {A[],B[],C[A,B]} ,A和B是无依赖的,C依赖任务A和B,那么任务调度器可以按照A、B、C的顺序进行调度,也可以按照B、A、C的顺序进行调度每个任务执行中,任务调度器都阻塞等待, 也可以让AB两个任务并发(需要分配到不同线程)阻塞等待AB均完成后调度C。在第一个版本设计中,我还没有采用这个方案,目前让库保持足够轻量。当存在多组初始化路径时,其复杂程度远大于本处的例子

有向无环图(DAG)

接下来我们适当花一些篇幅来讨论DAG。在我们上面提到的任务集这一角色中,我们使用了DAG来处理拓扑排序和依赖无环校验。

我们将任务看做是图中的顶点,任务的依赖关系看做是边,方向和依赖方向相反,即 A[B] 意味着有从B到A的边。将所有的任务合并起来后我们将得到一份有向图,显然,成环的依赖是不被允许的。

为了更好的理解,我们人为的添加一个虚拟的顶点Start,作为初始化任务集的第一个任务,将所有无依赖的任务人为添加一个前置依赖:Start。

一个合法的任务集,必然没有成环的依赖,所以一定不是强连通图,在我们添加了虚拟顶点start后,其基图一定是连通图,故而合法的任务集(包含虚拟Start节点)是一个弱连通图

环校验

我们采用DFS方式递归遍历,受益于我们制定的虚拟顶点Start,我们可以直接从这个顶点开始。

定义深度集合 deepPathList,选定起始顶点S, 定义回环顶点列表 loopbackList, 定义路径列表 pathList

直接上代码 getEdgeContainsPoint(startPoint, Type.X) 代表取出所有以startPoint为起始点的边

fun recursive(startPoint: T, pathList: MutableList<T>) {
    if (pathList.contains(startPoint)) {
          loopbackList.add("${debugPathInfo(pathList)}->${startPoint.let(nameOf)}")
          return
    }
    pathList.add(startPoint)
    val edgesFromStartPoint = getEdgeContainsPoint(startPoint, Type.X)
    if (edgesFromStartPoint.isEmpty()) {
        val descList: ArrayList<T> = ArrayList(pathList.size)
        pathList.forEach { path -> descList.add(path) {
        deepPathList.add(descList)
    }
    edgesFromStartPoint.forEach {
        recursive(it.to, pathList)
    }
    pathList.remove(startPoint)
} 

如果loopbackList不为空,则代表存在回环,回环的信息就存放在loopbackList中

契合需求的排序方式

上面我们已经提到了深度优先遍历(DFS),但是这种方式作出的拓扑排序不适合我们的需求,他适合寻找最优或者最差路径。而广度优先遍历(BFS)才契合需求。

直接给出代码:

private fun DAG<JOB>.bfs(): JobChunk {

    val zeroDeque = ArrayDeque<JOB>()
    val inDegrees = HashMap<JOB, Int>().apply {
        putAll(this@bfs.inDegreeCache)
    }
    inDegrees.forEach { (v, d) ->
        if (d == 0)
            zeroDeque.offer(v)
    }

    val head = JobChunk.head()
    var currentChunk = head
    val tmpDeque = ArrayDeque<JOB>()
    while (zeroDeque.isNotEmpty() || tmpDeque.isNotEmpty()) {
        if (zeroDeque.isEmpty()) {
            currentChunk = currentChunk.append()
            zeroDeque.addAll(tmpDeque)
            tmpDeque.clear()
        }
        zeroDeque.poll()?.let { vertex ->
            currentChunk.addJob(vertex)
 
            this.getEdgeContainsPoint(vertex, Type.X).forEach { edge ->
                inDegrees[edge.to] = (inDegrees[edge.to] ?: 0).minus(edge.weight).apply {
                    if (this == 0)
                        tmpDeque.offer(edge.to)
                }
            }
        }
    }
    return head
}

其中JubChunk是一组无关联的Job 即前文提到的初始化任务,前面提到目前没有让任务的执行可并发,JobChunk是为了可支持并发做准备的

关于DAG的部分我们就不再花篇幅介绍了,有兴趣的同学可以自行查阅相关资料

任务的描述

先上代码:

abstract class JOB {
    abstract val uniqueKey: String
    abstract val dependsOn: List<String>
    abstract val dispatcher: CoroutineDispatcher

    internal fun runInit(maat: Maat) {
        MainScope().launch {
            flow {
                init(maat)
                emit(true)
            }
                .flowOn(dispatcher)
                .catch {
                    maat.onJobFailed(this@JOB,it)
                }.flowOn(Dispatchers.Main)
                .collect {
                    maat.onJobSuccess(this@JOB)
                }
         }
    }
    abstract fun init(maat: Maat)
}

考虑到kotlin已经被官方推荐很长时间了,并且在去年Retrofit已经开始支持协程,姑且认为大部分项目中都已经开始使用协程了。所以很偷懒的直接使用了协程和Flow

  • uniqueKey 是当前任务名,需要人为确保唯一性
  • dependsOn 是当前任务所依赖的任务的uniqueKey的集合,虽然使用了List,但是顺序无关。
  • dispatcher 指定任务执行被分配到的线程类型
  • fun init(maat: Maat) 实际初始化逻辑,

注意:按需求分析初始化代码块是否需要 “同步、阻塞”,如果部分代码是“异步、基于回调”且无法更改,这个实际场景(必须要异步获取结果,且该结果被另一个组件使用)想来很少见,第一个版本中我没有考虑

示例代码模拟了4个初始化任务,有点长,具体的使用可以看一下Demo

val maat = Maat.init(application = this, printChunkMax = 6,
    logger = object : Maat.Logger() {
        override val enable: Boolean = true

        override fun log(msg: String, throws: Throwable?) {
            Log.d("maat", msg, throws)
        }
    }, callback = Maat.Callback(onSuccess = {}, onFailure = { maat, job, throwable ->
    })
)

maat.append(object : JOB() {
    override val uniqueKey: String = "a"
    override val dependsOn: List<String> = emptyList()
    override val dispatcher: CoroutineDispatcher = Dispatchers.IO
     
    override fun init(maat: Maat) {
        Log.e(
            "maat",
            "run:" + uniqueKey + " isMain:" + (Looper.getMainLooper() == Looper.myLooper())
        )
        //test exception
// throw NullPointerException("just a test")
    }

    override fun toString(): String {
        return uniqueKey
    }
}).append(object : JOB() {
    override val uniqueKey: String = "b"
    override val dependsOn: List<String> = arrayListOf("a")
    override val dispatcher: CoroutineDispatcher = Dispatchers.Main /* + Job()*/

    override fun init(maat: Maat) { 
        Log.e(
            "maat",
            "run:" + uniqueKey + " isMain:" + (Looper.getMainLooper() == Looper.myLooper())
        }
    }

    override fun toString(): String {
        return uniqueKey
    }

}).append(object : JOB() {
    override val uniqueKey: String = "c"
    override val dependsOn: List<String> = arrayListOf("a")
    override val dispatcher: CoroutineDispatcher = Dispatchers.IO /* + Job()*/

    override fun init(maat: Maat) {
        Log.e(
            "maat",
            "run:" + uniqueKey + " isMain:" + (Looper.getMainLooper() == Looper.myLooper())
        }
    }

    override fun toString(): String {
        return uniqueKey
    }

}).append(object : JOB() {
    override val uniqueKey: String = "d"
    override val dependsOn: List<String> = arrayListOf("a", "b", "c")
    override val dispatcher: CoroutineDispatcher = Dispatchers.Main

    override fun init(maat: Maat) {
        Log.e(
            "maat",
            "run:" + uniqueKey + " isMain:" + (Looper.getMainLooper() == Looper.myLooper())
        }
    }

    override fun toString(): String {
        return uniqueKey
    }
}).start()

在JIMU中使用

JIMU是一种很彻底的组件化方案,意味着编写代码时存在代码边界,即使是空壳宿主和业务组件之间也存在。前面也提到了,JIMU是使用字节码技术织入的组件加载代码(设置为自动加载组件时),而织入的代码是在Application的onCreate最后执行。

这这一前提下,如果通过javasist实现Maat的任务设置部分,他的可维护性将很差。所以我建议将任务设置部分放在组件的初始化入口处,这样可读性和可维护性都相对好一点.

以原先的分享业务组件为例:

public class ShareApplike implements IApplicationLike {

    UIRouter uiRouter = UIRouter.getInstance();

    @Override
    public void onCreate() {
        uiRouter.registerUI("share");
        Log.e("share","share on create");
        Maat.Companion.getDefault().append(new JOB() {
            @NotNull
            @Override
            public String getUniqueKey() {
                return "share";
            }

            @NotNull
            @Override
            public List<String> getDependsOn() {
                return Collections.singletonList("reader");
            }

            @NotNull
            @Override
            public CoroutineDispatcher getDispatcher() {
                return Dispatchers.getMain();
            }

            @Override
            public void init(@NotNull Maat maat) {
                Log.d("share", "模拟初始化share,context:" +maat.getApplication().getClass().getName());
            }

            @Override
            public String toString() {
                return getUniqueKey();
            }

    }
 
    @Override
    public void onStop() {
        uiRouter.unregisterUI("share");
    }
}

当然,务必不要忘记在Application的onCreate()中先初始化Maat:

Maat.Companion.init(this, 8, new Maat.Logger() {
            @Override
            public boolean getEnable() {
                return true;
            }

            @Override
            public void log(@NotNull String s, @Nullable Throwable throwable) {
                if (throwable != null) {
                    Log.e("maat",s,throwable);
                } else {
                    Log.d("maat",s);
                }
            }
}, new Maat.Callback(new Function1<Maat, Unit>() {
            @Override
            public Unit invoke(Maat maat) {
                Maat.Companion.release();
                return null;
            }
}, new Function3<Maat, JOB, Throwable, Unit>() {
            @Override
            public Unit invoke(Maat maat, JOB job, Throwable throwable) {
                return null;
             }
}

而Maat的启动API调用,自然由javasist织入了。配合最新的gradle插件 build-gradle:1.3.4方可使用,启用开关 为:

combuild {
    useMaat = true/false
}

关注公众号:Android苦做舟 解锁 《Android十二大板块PDF》 音视频大合集,从初中高到面试应有尽有;让学习更贴近未来实战。已形成PDF版

十二个模块PDF内容如下

1.2022最新Android11位大厂面试专题,128道附答案 2.音视频大合集,从初中高到面试应有尽有 3.Android车载应用大合集,从零开始一起学 4.性能优化大合集,告别优化烦恼 5.Framework大合集,从里到外分析的明明白白 6.Flutter大合集,进阶Flutter高级工程师 7.compose大合集,拥抱新技术 8.Jetpack大合集,全家桶一次吃个够 9.架构大合集,轻松应对工作需求 10.Android基础篇大合集,根基稳固高楼平地起 11.Flutter番外篇:Flutter面试+项目实战+电子书 12.大厂高级Android组件化强化实战

整理不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔

转载自:https://juejin.cn/post/7159500422959333389
评论
请登录