*本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布html
CC:Component Caller,一个android组件化开发框架, 已开源,github地址:github.com/luckybilly/… 本文主要讲解框架实现原理,若是只是想了解一下如何使用,可直接到github上查看README文档java
首先说明一下,本文将讲述的组件化与业内的插件化(如:Atlas, RePlugin等)不是同一个概念android
组件化开发:就是将一个app分红多个Module,每一个Module都是一个组件(也能够是一个基础库供组件依赖),开发的过程当中咱们能够单独调试部分组件,组件间不须要互相依赖,但能够相互调用,最终发布的时候全部组件以lib的形式被主app工程依赖并打包成1个apk。git
插件化开发:和组件化开发略有不用,插件化开发时将整个app拆分红不少模块,这些模块包括一个宿主和多个插件,每一个模块都是一个apk(组件化的每一个模块是个lib),最终打包的时候将宿主apk和插件apk(或其余格式)分开或者联合打包。github
本文将主要就如下几个方面进行介绍:web
1、为何须要组件化?数据库
2、CC的功能介绍api
3、CC技术要点安全
4、CC执行流程详细解析服务器
5、使用方式介绍
6、老项目进行组件化改造的成本有多高?
关于使用组件化的理由,上网能搜到不少,如业务隔离、单独以app运行能提升开发及调试效率等等这里就很少重复了,我补充一条:组件化以后,咱们能很容易地实现一些组件层面的AOP,例如:
组件A打包在主app中,组件B为单独运行的组件app,下图演示了在主app中调用二者的效果,并将结果以Json的格式显示在下方。demo下载地址):
实现CC组件化开发框架主要须要解决的问题有如下几个方面:
为了减小后期维护成本,想要实现的效果是:当须要添加某个组件到app时,只须要在gradle中添加一下对这个module的依赖便可(一般都是maven依赖,也能够是project依赖)
最初想要使用的是annotationProcessor经过编译时注解动态生成组件映射表代码的方式来实现。但尝试事后发现行不通,由于编译时注解的特性只在源码编译时生效,没法扫描到aar包里的注解(project依赖、maven依赖均无效),也就是说必须每一个module编译时生成本身的代码,而后要想办法将这些分散在各aar种的类找出来进行集中注册。
ARouter的解决方案是:
运行时经过读取全部dex文件遍历每一个entry查找指定包内的全部类名,而后反射获取类对象。这种效率看起来并不高。
ActivityRouter的解决方案是(demo中有2个组件名为'app'和'sdk'):
在主app module中有一个@Modules({"app", "sdk"})
注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类
在RouterInit类的init方法中生成调用同一个包内的RouterMapping_app.map
每一个module生成的类(RouterMapping_app.java 和 RouterMapping_sdk.java)都放在com.github.mzule.activityrouter.router包内(在不一样的aar中,但包名相同)
在RouterMapping_sdk类的map()方法中根据扫描到的当前module内全部路由注解,生成了调用Routers.map(...)方法来注册路由的代码
在Routers的全部api接口中最终都会触发RouterInit.init()方法,从而实现全部路由的映射表注册
这种方式用一个RouterInit类组合了全部module中的路由映射表类,运行时效率比扫描全部dex文件的方式要高,但须要额外在主工程代码中维护一个组件名称列表注解: @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,现已开源,并将功能升级为编译时自动扫描任意指定的接口实现类(或类的子类)并自动注册到指定类的指定方法中。只须要在app/build.gradle中配置一下扫描的参数,没有任何代码侵入,原理详细介绍传送门
经过实现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进行组件调用呢?
目前,常见的组件化框架采用的跨app通讯解决方案有:
设计此功能时,个人出发点是:做为组件化开发框架基础库,想尽可能让跨进程调用与在进程内部调用的功能一致,对使用此框架的开发者在切换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'
或apply plugin: 'com.android.library'
以及sourceSets的切换。 为了不在每一个module的build.gradle中配置太多重复代码,我作了个封装,默认为library模式,提供2种方式切换为application模式:在module的build.gradle中添加ext.runAsApp = true
或在工程根目录中local.properties中添加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'
复制代码
android的startActivityForResult的设计也是为了页面传值,在CC组件化框架中,页面传值根本不须要用到startActivityForResult,直接做为异步实现的组件来处理(在原来setResult的地方调用CC.sendCCResult(callId, ccResult)
,另外须要注意:按back键及返回按钮的状况也要回调结果)便可。
若是是原来项目中存在大量的startActivityForResult代码,改形成本较大,能够用下面这种方式来保留原来的onActivityResult(...)及activity中setResult相关的代码:
在原来调用startActivityForResult的地方,改用CC方式调用,将当前context传给组件
CC.obtainBuilder("demo.ComponentA")
.setContext(context)
.addParams("requestCode", requestCode)
.build()
.callAsync();
复制代码
在组件的onCall(cc)方法中用startActivityForResult的方式打开Activity
@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生命周期关联
在api level 14 (android 4.0)以上能够经过注册全局activity生命周期回调监听,在onActivityDestroyed
方法中找出全部此activity关联且未完成的cc对象,并自动调用取消功能:
application.registerActivityLifecycleCallbacks(lifecycleCallback);
复制代码
android.support.v4.app.Fragment生命周期关联
support库从 25.1.0 开始支持给fragment设置生命周期监听:
FragmentManager.registerFragmentLifecycleCallbacks(callback)
复制代码
可在其onFragmentDestroyed
方法中取消未完成的cc调用
andorid.app.Fragment生命周期关联(暂不支持)
组件间通讯采用了组件总线的方式,在基础库的组件管理类(ComponentMananger)中注册了全部组件对象,ComponentMananger经过查找映射表找到组件对象并调用。
当ComponentMananger接收到组件的调用请求时,查找当前app内组件清单中是否含有当前须要调用的组件
没有:执行App之间CC调用的流程
java.util.concurrent.Callable
接口ChainProcessor
类来负责具体组件的调用
ChainProcessor.call()
来调用组件,并将CCResult直接返回给调用方ChainProcessor
放入线程池中执行,经过IComponentCallback.onResult(cc, ccResult)
将CCResult回调给调用方执行过程以下图所示:
ICCInterceptor
)实现原理全部拦截器按顺序存放在调用链(Chain)中
在自定义拦截器以前有1个CC框架自身的拦截器:
ValidateInterceptor
在自定义拦截器以后有2个CC框架自身的拦截器:
LocalCCInterceptor
(或RemoteCCInterceptor
)Wait4ResultInterceptor
Chain类负责依次执行全部拦截器interceptor.intercept(chain)
拦截器intercept(chain)
方法经过调用Chain.proceed()
方法获取CCResult
当要调用的组件在当前app内部时,执行此流程,完整流程图以下:
CC的主体功能由一个个拦截器(ICCInterceptor
)来完成,拦截器造成一个调用链(Chain
),调用链由ChainProcessor启动执行,ChainProcessor对象在ComponentManager中被建立。 所以,能够将ChainProcessor看作一个总体,由ComponentManager建立后,调用组件的onCall方法,并将组件执行后的结果返回给调用方。 ChainProcessor内部的Wait4ResultInterceptor
ChainProcessor的执行过程能够被timeout和cancel两种事件停止。
当要调用的组件在当前app内找不到时,执行此流程,完整流程图以下:
CC的集成很是简单,仅需4步便可完成集成:
添加自动注册插件
buildscript {
dependencies {
classpath 'com.billy.android:autoregister:1.0.4'
}
}
复制代码
引用apply cc-settings.gradle文件代替 'app plugin ...'
apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'
复制代码
实现IComponent接口建立一个组件类
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
PS:配合个人另外一个库(PreLoader)一块儿食用味道更佳:AOP实如今打开页面以前预加载页面所需的数据,而且这个预加载功能彻底在组件内部实现,与外部无耦合。
有些同窗很想尝试组件化开发,但仅仅停留在了解的阶段,缘由是担忧在老项目上进行改造的工程量太大,不敢大改。
CC框架自己就是在老项目进行组件化改造的需求下设计出来的,考虑到了组件化过程当中的一些痛点:
apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'
便可module名称=true
便可。几乎零门槛进行组件化开发本文比较详细地介绍了android组件化开发框架《CC》的主要功能、技术方案及执行流程,并给出了使用方式的简单示例。 你们若是感兴趣的话能够从GitHub上clone源码来进行具体的分析,若是有更好的思路和方案也欢迎贡献代码进一步完善CC。