前语
忽如一夜冬风来这天气刚好碰到周五,适宜在家里蛰伏八。
昨日剖析了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
1.Android彻底组件化Demo发布
1.1.JIMU运用指南
首要咱们看一下demo的代码结构,然后依据这个结构图再次从独自调试(发布)、组件交互、UI跳转、集成调试、代码鸿沟和生命周期等六个方面深入剖析,之所以说“再次”,是由于上一篇文章咱们现已讲了这六个方面的原理,这篇文章更侧重其详细完结。
代码中的各个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插件来静静完结。
组件交互
在这儿组件的交互专指组件之间的数据传输,在咱们的计划中运用的是接口+完结的方法,组件之间彻底面向接口编程。
在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检查。
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的四种语法的功用和代码阻隔作用见下图:
从上图能够看出,在代码阻隔作用上,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功用区的图如下:
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组件化强化实战
收拾不易,重视一下吧。ღ( ・ᴗ・` )