https://github.com/luckybilly/CC
本文主要讲解框架实现原理,如果只是想了解一下如何使用,可直接到github上查看README文档
前言
首先说明一下,本文将讲述的组件化与业内的插件化(如:Atlas, RePlugin等)不是同一个概念
本文将主要就以下几个方面进行介绍:
一、为什么需要组件化?
二、CC的功能介绍
三、CC技术要点
四、CC执行流程详细解析
五、使用方式介绍
六、老项目进行组件化改造的成本有多高?
关于使用组件化的理由,上网能搜到很多,如业务隔离、单独以app运行能提高开发及调试效率等等这里就不多重复了,我补充一条:组件化之后,我们能很容易地实现一些组件层面的AOP,例如:
1.支持组件间相互调用(不只是Activity跳转,支持任意指令的调用/回调)
2.支持组件调用与Activity、Fragment的生命周期关联
3.支持app间跨进程的组件调用(组件开发/调试时可单独作为app运行)
4.支持app间调用的开关及权限设置(满足不同级别的安全需求,默认打开状态且不需要权限)
5.支持同步/异步方式调用
6.支持同步/异步方式实现组件
7.调用方式不受实现方式的限制(例如:可以异步调用另一个组件的同步实现功能。注:不要在主线程同步调用耗时操作)
8.支持添加自定义拦截器(按添加的先后顺序执行)
9.支持超时设置
10.支持手动取消
11.编译时自动注册组件(IComponent),无需手动维护组件注册表(使用ASM修改字节码的方式实现)
12.支持动态注册/反注册组件(IDynamicComponent)
13.支持组件间传递Fragment等非基础类型的对象(组件在同一个app内时支持、跨app传递非基础类型的对象暂不支持)
14.尽可能的解决了使用姿势不正确导致的crash:
组件A打包在主app中,组件B为单独运行的组件app,下图演示了在主app中调用两者的效果,并将结果以Json的格式显示在下方。demo下载地址):
实现CC组件化开发框架主要需要解决的问题有以下几个方面:
为了减少后期维护成本,想要实现的效果是:当需要添加某个组件到app时,只需要在gradle中添加一下对这个module的依赖即可(通常都是maven依赖,也可以是project依赖)
最初想要使用的是annotationProcessor通过编译时注解动态生成组件映射表代码的方式来实现。但尝试过后发现行不通,因为编译时注解的特性只在源码编译时生效,无法扫描到aar包里的注解(project依赖、maven依赖均无效),也就是说必须每个module编译时生成自己的代码,然后要想办法将这些分散在各aar种的类找出来进行集中注册。
ARouter的解决方案是:
然后在运行时通过读取每个dex文件中的这个包下的所有类通过反射来完成映射表的注册,详见ClassUtils.java源码
运行时通过读取所有dex文件遍历每个entry查找指定包内的所有类名,然后反射获取类对象。这种效率看起来并不高。
ActivityRouter的解决方案是(demo中有2个组件名为'app'和'sdk'):
@Modules({"app", "sdk"})
注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类在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'
注: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调用
组件间通信采用了组件总线的方式,在基础库的组件管理类(ComponentMananger)中注册了所有组件对象,ComponentMananger通过查找映射表找到组件对象并调用。
当ComponentMananger接收到组件的调用请求时,查找当前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。
CC:安全地支持跨App调用的Android组件化开发框架(不只是路由哦)
CC框架实践(1):实现登录成功再进入目标界面功能
CC框架实践(2):Fragment和View的组件化
CC框架实践(3): 让jsBridge更优雅
ActivityRouter
ARouter
ModularizationArchitecture
Android架构思考(模块化、多进程)
开源最佳实践:Android平台页面路由框架ARouter
DDComponentForAndroid
Router