前语

忽如一夜冬风来这天气刚好碰到周五,适宜在家里蛰伏八。

昨日剖析了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组件化强化实战

收拾不易,重视一下吧。ღ( ・ᴗ・` )