Android组件化

1、组件化和插件化区别:

组件化开发:

(1)、组件化是将一个app分成多个Module,每个Module都是一个组件;
(2)、组件化在发布的时候,所有组件以lib的形式被主app工程依赖,并打包成1个apk,不支持动态加载。

插件化开发:

(1)、插件化是将整个app拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk;
(2)插件化在发布的时候,可以将宿主apk和插件apk,分开或者联合打包,在运行时,宿主apk可以动态加载某个插件apk。

相同点:

(1)、两者都是为了使项目模块化解耦;
(2)、二者中组件或者插件都可以单独调试。

2、组件化的优点和缺点:

2.1、组件化的有点

(1)、业务隔离;
(2)、组件单独运行提高编译速度
(3)、组件化之后,很容易地实现一些组件层面的AOP,比如打开页面的时候进行异步数据加载;

2.2组件化的缺点:

(1)、每个组件都需要依赖公用库,会拖慢整体编译速度;

3、CC组件化原理

1、组件化优点之一是解耦,cc如何实现解耦
2、解耦之后的问题是组件之间如何进行通信(app之内的通信,和跨app通信)
3、既然后跨app通信,如何将组件单独运行,以及在A,L之间智能切换
4、组件注册和反注册过程繁琐,如何实现自动注册
5、如何监控调用超时以及取消,如何将调用和activity生命周期关联

cc组件化中,组件之间通信采用了组件总线通信的方式,在组件管理类(ComponentMananger)中注册了所有组件对象,当请求某个组件服务时,ComponentMananger通过查找映射表找到组件对象并调用并返回结果,它支持同步异步调用组件和同步异步实现组件,并且支持跨app调用组件。

3.1、app内部调用流程是

app内部调用流程.png

当组件a需要请求组件b的服务时,它就通过cc去调用cm,然后cm查找路由表,找到对应组件,进行调用,最后将结果返回给组件a。

3.2、跨app调用流程

跨app调用流程.png

当app1中的组件a请求app2中的组件B的服务时,它依然是通过cc调用cm,cm路由表中不存在组件B,它会通过BroadReceiver+Localsocket和app2建立通信,然后app2中的cm去查找自身的路由表,如果存在组件B,就就行调用,然后将结果通过LocalSocke返回给组件a。

3.3、app内部,组件之间的具体调用流程

app内部组件调用流程.png

3.4、跨app调用流程

1.0版本跨app组件调用流程
2.0版本跨app组件调用流程

这里有一个问题是如何知道组件在哪个进程?

前提:组件注册时会将组件名称及其对应的进程记录在一个map中

情况1:同一个app中,跨进程调用时,直接(调用ComponentManager.getComponentProcessName(componentName),)查询这个map,就可以获取组件所对应的进程。

情况2:跨app调用时,没法直接(调用ComponentManager.getComponentProcessName(componentName)查询调用方的这个map,去获取组件对应的进程名。因为组件没有在调用方的app中注册。

跨app调用时,获取组件对应进程的具体过程是:
1、app启动时,获取到手机中所有应用了cc框架的app,以及它对应的包名(这个包名即主进程名)
2、获取通过ContentProvider获取这些app主进程中的IRemoteCCService。
3、通过这个主进程的IRemoteCCService去查询对应app中,组件对应进程的map,就可以获取组件对应的进程名。

如何知道哪些app应用了cc框架?
在cc框架中内置一个RemoteConnectionActivity,并给其指定一个intent-filter,
在调用方app启动的时候,使用PackageManager去查询这个activity对应的activityinfo,继而通过这个ActivityInfo获取到对应packageName。

另外会通过广播监听app的安装,可直接获取app对应的包名,通过判断是否能启动RemoteConnectionActivity,判断是否应用了cc框架。如果是就继续进行2,3两步流程。

3.5、cc监控策略

在ChainProcessor开启拦截器链的之前是调用CCMonitor.addMonitorFor(cc);将此次调用流程加入到监控队列中。

超时监控

在CCMonitor中维护这一个ConcurrentHashMap<String, CC>,并且使用minTimeoutAt记录ConcurrentHashMap中cc的最短超时时间,当ConcurrentHashMap不为空时,就会开启一个监控线程去检测这个map中的cc,具体的过程就是监控线程先阻塞minTimeoutAt秒,然后去遍历map,超时cc从map中去除并调用cc.timeout,终止调用流程(设置将finished设为ture,每个拦截器调用chain.proceed之前会检测finished,如果为true就终止流程),然后在遍历的过程中筛选出队列中剩余最短的超时时间minTimeoutAt,再次循环检测。直到map为空,监控线程停止。(注意:当新加入的cc超时时间比minTimeoutAt小时,主动唤醒监控线程)。

在CCMonitor中会将所有cc加入ConcurrentHashMap<String, CC>,并开启一个异步线程去监控cc超时:
map中有监控对象时,循环遍历map,统计出下次需要进行超时调用操作距现在的时间间隔,进入wait状态,时间到后,被唤醒进行超时调用,并且将超时cc从map中移除,然后再次计统计出下次需要进行超时调用操作距现在的时间间隔,再次进入状态,以此循环。
直到map为空,跳出循环,当有监控对象加入时再次开启上述循环

和Activity、Fragment的生命周期关联

通过application.registerActivityLifecycleCallbacks(new CCMonitor.ActivityMonitor())注册一个自定义的
ActivityLifecycleCallbacks,在其回调方法onActivityDestroyed方法中遍历持有cc对象的ConcurrentHashMap,如果当前Destroye的activity和cc中传入的cancelOnDestroyActivity相同就调用cc的cancel方法,取消整个调用流程。

Fragment类似
通过FragmentManager.registerFragmentLifecycleCallbacks注册一个自定义的FragmentLifecycleCallbacks。

4、cc-register

4.1、作用1:将module在application和library之间自动切换

4.1.1、现象:

所有应用cc-register插件的module,
(1)、可直接点击运行按钮单独运行;
(2)、当moduleA依赖moduleB,给moduleA打包时, moduleB自动变成library被依赖。

4.1.2、原理:

判断当前执行的task:
(1、如果当前module是主app(在主app中使用ext.mainapp=true声明),则应用application插件;)
(2、否则,如果当前module是library或者在local.properties声明打包,则应用library插件)
3、否则,如果当前task是对当前module集成打包或者当前task不是集成打包的task,则应用application插件,否则应用library插件
这样就保证了
1、 如果当前task是集成打包的task,并且不是为该module打包,那么该module是应用的library插件,可以被依赖;
2、如果当前task不是集成打包的task,module默认都是应用的application插件,可以直接运行 )

(打开项目会执行构建任务,不是集成打包任务,默认给所有module应用application插件)

如何判断当前task是为本module做集成打包?

读取当前task,如果当前任务是ASSEMBLE|(BUILD)|(INSTALL)中的一种,并且前缀是当前module名,则是为当前module打包。

有些集成打包task名称前缀没有module名称,比如build,但是主app的build task本身是给主app打包,所以这里也需要给主app默认应用application插件(在主app的build.gradle的ext块中声明一个字段,然后读取这字段判断)。

然后根据module应用的是application插件还是library插件,使用sourceSets来给module应用不同的menifest文件。

4.1.3、如何做到module完全隔离
提供了addComponent dependencymodule 方法,只有在给当前module集成打包的时候才去依赖dependencymodule(dependencymodule没有在localProperties文件中声明排除),保证了组件化完全隔离。

4.2、作用2:组件自动注册

使用Transform API 结合ASM进行字节码插桩,在打包过程中,class文件转换dex的阶段,会经历多个Transform,我们可以自定义一个Transform得到所有的class文件,然后使用ASM对其进行操作。
在plugin的apply注册一个自定义的Transform类,在其transform方法中去进行扫描和插入。

4.2.1、扫描过程:

扫描所有jar文件中的class文件和本地目录中的class文件,
1、 根据类名比较找到要被插入代码的类并记录;
2、通过ASM中的classvisitor找到其实现了Icomponet接口的子类,并记录。
通过构造一个自定义的ScanClassVisitor,然后将class文件传入,(调用ClassReader的accept方法),就会回调自定义的ClassVisitor中的visit方法,在visit方法中我们可以得到当前类的父类和接口,然后进行判断即可。

4.2.2、插入过程

如果要被插入代码的类在jar文件中:

建立一个(optJar的)临时文件,)然后用这个文件构建一个JarOutputStream,)遍历Jar文件中的class文件,如果是被注入注册代码的类,就获取代码插入之后的字节码数组,写入(optJar)临时文件,如果不是就直接写入,最后将原jar文件删除,临时文件(optJar)重命名为原jar文件的名称。

如果要被插入代码的类是class文件

建立一个(optJar的)临时文件,获取代码插入之后的字节码数组,然后写入临时文件(optJar),最后删除原class文件,将临时文件(optJar)重命名为原class文件的名称。

使用asm插入字节码的具体过程

自定义一个classvisitor,将我们的要插入字节码class文件传入,在其visitMethod方法中判断是否是要被插入的方法,如果是就返回一个自定义的MethodVisitor,在自定义的MethodVisitor 的回调方法visitInsn中就可以将我们收集的Icomponet的子类构造一个对象调用相应方法进行插入。

总结:
添加一个组件分为两步:
(1)、在build.gradle对组件对应的module进行依赖
(2)、调用ComponetManager.registerComponet(Icomponent c)方法注册我们的组件。
一个前提是:cc中组件的概念是自定义一个类实现Icomponent接口就是一个组件。
这里的注册组件就是注册我们自定义的Icomponent的子类。

cc为了减少后期维护成本。它使用AOP的方式帮我们自动注册了组件。
具体的过程是在打包过程中有一个阶段是class文件转换dex的阶段,所有class文件会经历多个Transform进行处理,我们可以自定义一个Transform得到所有的class文件,然后扫描得到所有IComponent的子类,使用ASM操作字节码去调用注册方法进行注册。
1、怎么识别IComponent的子类
2、asm插入字节码的具体过程

5、cc组件化框架的优势

cc,DDComponentForAndroid,ModularizationArchitecture,arouter
从组件化的几个好处来看
1、代码解耦,业务隔离:
业务隔离其实不需要组件化框架,把一个组件的代码拷贝到一个module中,然后可以人为控制两个组件不耦合,但是这种控制可能是不可靠的,比如,
cc和DDComponentForAndroid在代码层面做限制,避免了各个模块之间的耦合,原理是,
arouter和ModularizationArchitecture并没有提供提这种代码层面的解耦限制。

2、加快编译速度-单独运行
2.1、各个模块可以单独运行以及被依赖
让module单独运行的原理,就是让它应用application插件以及配置对应的menifest;
组件有单独运行,必然也有可被依赖。它要被依赖的时候又要去给它应用library插件,以及配置library对应的menifest,过程繁琐。
cc和DDComponentForAndroid都做到了自动切换。现象是。。。
原理是。。。
ModularizationArchitecture和arouter都没有提供这种切换方式,由开发者自行解决。
2.2、加快编译速度-联合调试
module A 依赖 module B, 此时在module B
上开发一个功能,但是必须通过module A才能进入module B看到这个功能,,调试时,一种方式是将module A
和module B集成打包,一种方式是将module A和module B分开打包,module A通过跨app的方式和module B进行通信。由于我们只在module B开发功能,需要反复编译的是module B。集成打包所消耗的编译时间是(module A+module B)n,分开打包的编译时间是moduleA+module Bn。可以看出分开打包编译时间比集成打包编译时间缩减了(n-1)*module A。

比如首页和商户,首页单独运行调试没问题,但是调试商户时,需要从首页进去,这时就有两个选择,一个是首页商户联合打包,一个是首页商户分开打包,调试商户时,从首页组件跨app调用商户组件。在编译时间方面显然是后一种方式比较好。

然后在看哪些组件化框架支持跨app通信呢?
cc和ModularizationArchitecture,都是通过aidl实现跨进程通信,原理是,
DDComponentForAndroid无法跨app通信,
arouter使用URLScheme结合一个SchemeFilterActivity可以做到跨进程通信(通过URLScheme启动另外一个app中的SchemeFilterActivity,在这个activity中使用arouter进行分发),它的缺点是1、需要中转activity;2、不可利于数据回传;3、如果设备上安装了多个相同URLScheme的app,会弹出选择框;4、无法进行权限设置,无法进行开关设置,任意app都可调用,存在安全性风险

另外的亮点:
1、 cc是基于拦截器模式,容易扩展或实现一些组件层面的aop,比如页面跳转时异步请求数据。
2、使用字节码插桩自动注册组件,代替手动注册
3、支持超时,取消,并且和activity的生命周期关联

6、自己思考的组件化方案-缩短编译时间

不需要将组件单独打成apk,
1、自定义一个gradle插件,功能是可以将组件打成aar包,然后上传到maven库
2、然后各方向负责人只需要维护一个app壳module,和对应负责业务的module,其他的组件从maven依赖。
3、这样我们的编译时间就是app的编译时间加上一个业务module的编译时间。
4、发版上线的时候所有业务负责人发版最新的aar,然后集成到app中进行打包。

优点:
1、避免了跨进程通信的麻烦
2、不需要每个组件都依赖公共库

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容

  • (转载) Android组件化方案已经开源,参见Android组件化方案开源。方案的解读文章是一个小的系列,这是系...
    江左灬梅郎阅读 2,772评论 2 31
  • 在目前移动互联网时代,每个 APP 就是流量入口,与过去 PC Web 浏览器时代不同的是,APP 的体验与迭代速...
    斜杠时光阅读 13,884评论 4 139
  • 1. 天色将晚的时候,我扛上锄头往家走。 暮色越过重重高山,整个世界将要被披上一层黑色的外衣。 老一辈常说,这个时...
    洛子帅阅读 1,247评论 42 43
  • 今天是我们导师班的毕业典礼,大家都好开心,临时我串了下主持人, 我也没准备,临时就上了, 感觉主持过程中有点不衔接...
    岳瑶阅读 115评论 1 2
  • 周五市场继续维持震荡走势,早上冲高,下午回落,尾盘继续上攻。在这样的震荡走势下,个股分化是较为明显的。 行情分析:...
    稳中有升阅读 223评论 0 0