*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
CC:Component Caller,一个android组件化开发框架, 已开源,github地址:https://github.com/luckybilly/CC 本文主要讲解框架实现原理,如果只是想了解一下如何使用,可直接到github上查看README文档
首先说明一下,本文将讲述的组件化与业内的插件化(如:Atlas, RePlugin等)不是同一个概念
【图片来源于网络】
组件化开发:就是将一个app分成多个Module,每个Module都是一个组件(也可以是一个基础库供组件依赖),开发的过程中我们可以单独调试部分组件,组件间不需要互相依赖,但可以相互调用,最终发布的时候所有组件以lib的形式被主app工程依赖并打包成1个apk。
插件化开发:和组件化开发略有不用,插件化开发时将整个app拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk(或其他格式)分开或者联合打包。
本文将主要就以下几个方面进行介绍:
一、为什么需要组件化? 二、CC的功能介绍 三、CC技术要点 四、CC执行流程详细解析 五、使用方式介绍
关于使用组件化的理由,上网能搜到很多,如业务隔离、单独以app运行能提高开发及调试效率等等这里就不多重复了,我补充一条:组件化之后,我们能很容易地实现一些组件层面的AOP,例如:
实现CC组件化开发框架主要需要解决的问题有以下几个方面:
为了减少后期维护成本,想要实现的效果是:当需要添加某个组件到app时,只需要在gradle中添加一下对这个module的依赖即可(通常都是maven依赖,也可以是project依赖)
最初想要使用的是annotationProcessor通过编译时注解动态生成组件映射表代码的方式来实现。但尝试过后发现行不通,因为编译时注解的特性只在源码编译时生效,无法扫描到aar包里的注解(project依赖、maven依赖均无效),也就是说必须每个module编译时生成自己的代码,然后要想办法将这些分散在各aar种的类找出来进行集中注册。
ARouter的解决方案是:
运行时通过读取所有dex文件遍历每个entry查找指定包内的所有类名,然后反射获取类对象。这种效率看起来并不高。
ActivityRouter的解决方案是(demo中有2个组件名为'app'和'sdk'):
注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类
- @Modules({"app", "sdk"})
还有没有更好的办法呢?
Transform API: 可以在编译时(dex/proguard之前)扫描当前要打包到apk中的所有类,包括: 当前module中java文件编译后的class、aidl文件编译后的class、jar包中的class、aar包中的class、project依赖中的class、maven依赖中的class。
ASM: 可以读取分析字节码、可以修改字节码
二者结合,可以做一个gradle插件,在编译时自动扫描所有组件类(IComponent接口实现类),然后修改字节码,生成代码调用扫描到的所有组件类的构造方法将其注册到一个组件管理类(ComponentManager)中,生成组件名称与组件对象的映射表。
此gradle插件被命名为:AutoRegister,现已开源,并将功能升级为编译时自动扫描接口实现类(或类的子类)并自动注册到指定类的指定方法中,原理详细介绍传送门
通过实现
接口同步返回结果来兼容同步/异步调用:
- java.util.concurrent.Callable
来获取返回结果
- CCResult result = Callable.call()
- ExecutorService.submit(callable)
调用组件的onCall方法时,可能需要异步实现,并不能同步返回结果,但同步调用时又需要返回结果,这是一对矛盾。 此处用到了Object的wait-notify机制,当组件需要异步返回结果时,在CC框架内部进行阻塞,等到结果返回时,通过notify中止阻塞,返回结果给调用方
注意,这里要求在实现一个组件时,必须确保组件一定会回调结果,即:需要确保每一种导致调用流程结束的逻辑分支上(包括if-else/try-catch/Activity.finish()-back键-返回按钮等等)都会回调结果,否则会导致调用方一直阻塞等待结果,直至超时。类似于向服务器发送一个网络请求后服务器必须返回请求结果一样,否则会导致请求超时。
市面上常见的组件化框架采用的通信解决方案有:
设计此功能时,我的出发点是:作为组件化开发框架基础库,想尽量让跨进程调用与在进程内部调用的功能一致,对使用此框架的开发者在切换app模式和lib模式时尽量简单,另外需要尽量不影响产品安全性。因此,跨组件间通信实现的同时,应该满足以下条件:
基于这些需求,我最终选择了BroadcastReceiver + Service + LocalSocket来作为最终解决方案:
- 如果appA内发起了一个当前app内不存在的组件: Component1,则建立一个LocalServerSocket,同时发送广播给设备上安装的其它同样使用了此框架的app,同时,若某个appB内支持此组件,则根据广播中带来的信息与LocalServerSocket建立连接,并在appB内调用组件Component1,并将结果通过LocalSocket发送给appA。
BroadcastReceiver是android四大组件之一,可以设置接收权限,能避免外部恶意调用。并且可以设置开关,接收到此广播后决定是否响应(假装没接收到...)。 之所以建立LocalSocket链接,是为了能继续给这次组件调用请求发送超时和取消的指令。
用这种方式实现时,遇到了3个问题:
关于切换方式在网络上有很多文章介绍,基本上都是一个思路:在module的build.gradle中设置一个变量来控制切换
或
- apply plugin: 'com.android.application'
以及sourceSets的切换。 为了避免在每个module的build.gradle中配置太多重复代码,我做了个封装,默认为library模式,提供2种方式切换为application模式:在module的build.gradle中添加
- apply plugin: 'com.android.library'
或在工程根目录中local.properties中添加
- ext.runAsApp = true
- module_name=true
使用这个封装只需一行代码:
- // 将原来的 apply plugin: 'com.android.application'或apply plugin: 'com.android.library'
- //替换为下面这一行
- apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'
注:cc-settings.gradle源码传送门
android的startActivityForResult的设计也是为了页面传值,在CC组件化框架中,页面传值根本不需要用到startActivityForResult,直接作为异步实现的组件来处理(在原来setResult的地方调用
,另外需要注意:按back键及返回按钮的情况也要回调结果)即可。
- CC.sendCCResult(callId, ccResult)
如果是原来项目中存在大量的startActivityForResult代码,改造成本较大,可以用下面这种方式来保留原来的onActivityResult(...)及activity中setResult相关的代码:
- CC.obtainBuilder("demo.ComponentA")
- .setContext(context)
- .addParams("requestCode", requestCode)
- .build()
- .callAsync();
- @Override
- public
- boolean
- onCall
- (CC cc)
- {
- Context context = cc.getContext();
- Object code = cc.getParams().get("requestCode");
- Intent intent = new Intent(context, ActivityA.class);
- if (!(context instanceof Activity)) {
- //调用方没有设置context或app间组件跳转,context为application
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
- if (context instanceof Activity && code != null && code instanceof Integer) {
- ((Activity)context).startActivityForResult(intent, (Integer)code);
- } else {
- context.startActivity(intent);
- }
- CC.sendCCResult(cc.getCallId(), CCResult.success());
- return false;
- }
为了适应不同需求,有2个安全级别可以设置:
- compile 'com.billy.android:cc:0.3.0'
可关闭响应外部调用
- CC.enableRemoteCC(false)
为了方便开发者接入,默认是开启了对外部组件调用的支持,并且不需要权限验证。app正式发布前,建议调用CC.enableRemoteCC(false)来关闭响应外部调用本app的组件。
背景:在使用异步调用时,由于callback对象一般是使用匿名内部类,会持有外部类对象的引用,容易引起内存泄露,这种内存泄露的情况在各种异步回调中比较常见,如Handler.post(runnable)、Retrofit的Call.enqueue(callback)等。
为了避免内存泄露及页面退出后取消执行不必要的任务,CC添加了生命周期关联的功能,在onDestroy方法被调用时自动cancel页面内所有未完成的组件调用
方法中找出所有此activity关联且未完成的cc对象,并自动调用取消功能:
- onActivityDestroyed
- application.registerActivityLifecycleCallbacks(lifecycleCallback);
可在其
- FragmentManager.registerFragmentLifecycleCallbacks(callback)
方法中取消未完成的cc调用
- onFragmentDestroyed
组件间通信采用了组件总线的方式,在基础库的组件管理类(ComponentMananger)中注册了所有组件对象,ComponentMananger通过查找映射表找到组件对象并调用。
当ComponentMananger接收到组件的调用请求时,查找当前app内组件清单中是否含有当前需要调用的组件
接口
- java.util.concurrent.Callable
类来负责具体组件的调用
- ChainProcessor
来调用组件,并将CCResult直接返回给调用方
- ChainProcessor.call()
放入线程池中执行,通过
- ChainProcessor
将CCResult回调给调用方
- IComponentCallback.onResult(cc, ccResult)
执行过程如下图所示:
)实现原理
- ICCInterceptor
- ValidateInterceptor
(或
- LocalCCInterceptor
)
- RemoteCCInterceptor
- Wait4ResultInterceptor
- interceptor.intercept(chain)
方法通过调用
- intercept(chain)
方法获取CCResult
- Chain.proceed()
当要调用的组件在当前app内部时,执行此流程,完整流程图如下:
CC的主体功能由一个个拦截器(
)来完成,拦截器形成一个调用链(
- ICCInterceptor
),调用链由ChainProcessor启动执行,ChainProcessor对象在ComponentManager中被创建。 因此,可以将ChainProcessor看做一个整体,由ComponentManager创建后,调用组件的onCall方法,并将组件执行后的结果返回给调用方。 ChainProcessor内部的
- Chain
ChainProcessor的执行过程可以被timeout和cancel两种事件中止。
- Wait4ResultInterceptor
当要调用的组件在当前app内找不到时,执行此流程,完整流程图如下:
CC的集成非常简单,仅需4步即可完成集成:
- buildscript {
- dependencies {
- classpath 'com.billy.android:autoregister:1.0.4'
- }
- }
- apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'
- public
- class
- ComponentA
- implements
- IComponent
- {
- @Override
- public String getName() {
- //组件的名称,调用此组件的方式:
- // CC.obtainBuilder("demo.ComponentA").build().callAsync()
- return "demo.ComponentA";
- }
- @Override
- public
- boolean
- onCall
- (CC cc)
- {
- Context context = cc.getContext();
- Intent intent = new Intent(context, ActivityComponentA.class);
- if (!(context instanceof Activity)) {
- //调用方没有设置context或app间组件跳转,context为application
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
- context.startActivity(intent);
- //发送组件调用的结果(返回信息)
- CC.sendCCResult(cc.getCallId(), CCResult.success());
- return false;
- }
- }
调用组件
- CC.obtainBuilder("component_name").build().call()
- //同步调用,直接返回结果
- CCResult result = CC.obtainBuilder("demo.ComponentA").build().call();
- //或 异步调用,不需要回调结果
- CC.obtainBuilder("demo.ComponentA").build().callAsync();
- //或 异步调用,在子线程执行回调
- CC.obtainBuilder("demo.ComponentA").build().callAsync(new IComponentCallback(){...});
- //或 异步调用,在主线程执行回调
- CC.obtainBuilder("demo.ComponentA").build().callAsyncCallbackOnMainThread(new IComponentCallback(){...});
更多用法请看github上的README
本文比较详细地介绍了android组件化开发框架《CC》的主要功能、技术方案及执行流程,并给出了使用方式的简单示例。 大家如果感兴趣的话可以从GitHub上clone源码来进行具体的分析,如果有更好的思路和方案也欢迎贡献代码进一步完善CC。
ActivityRouter ARouter ModularizationArchitecture Android架构思考(模块化、多进程) 开源最佳实践:Android平台页面路由框架ARouter
来源: https://juejin.im/post/5a2b92e4f265da430d57f047