昨天学完插件化,今天再来点组件化,多多益善!
/ 今日科技快讯 /
8月28日,第三届全球女性创业者大会在杭州召开,马云以联合国可持续发展目标倡导者的身份向到场的女性创业者表达敬意,并作出一个预测:未来30年是体验时代,女人会越来越厉害,就算到了机器人的世界,决定美和好标准的,依然是女人。
/ 作者简介 /
本篇文章来自啊森弟的投稿,和大家一起分析组件化框架CC的原理,希望对大家有所帮助!同时也感谢作者贡献的精彩文章。
啊森弟的博客地址:
https://blog.csdn.net/A_sendy
/ 前言 /
CC采用组件总线的方案,可以让你在对老项目进行组件化时不需要一下子拆分成一个个组件,反而这是一个渐进的过程,也就是文档所说的渐进式组件化。既然它这么优秀,那我们在使用它时,学下它的原理应该会有很多的收获。
CC的项目地址:
https://github.com/luckybilly/CC
CC的文档地址:
https://qibilly.com/CC-website
/ 开始 /
发起调用
在开始跟踪调用流程之前,我们先来看一张来自文档网站的图片:
根据图片可以知道CC的组件调用期间就是通过一系列的拦截器进行的,接下来开启它的调用流程。
在开始调用的时候,call表示同步调用,callAsync表示发起异步的调用,这两个方法会做一些参数的初始化和超时默认的设置,最终都会调用ComponentManager的call(this)方法:
static CCResult call(CC cc) {
//1.添加自定义的拦截器
chain.addInterceptors(cc.getInterceptors());
//2. 有效性校验放在自定义拦截器之后执行,优先执行自定义拦截器,让其可以拦截到所有组件调用
// 执行实际调用的拦截器在校验有效性结束后再添加
chain.addInterceptor(ValidateInterceptor.getInstance());
ChainProcessor processor = new ChainProcessor(chain);
//异步调用,放到线程池中运行
if (cc.isAsync()) {
//...
CC_THREAD_POOL.submit(processor);
//异步调用时此方法返回null,CCResult通过callback回调
return null;
} else {
//同步调用,直接执行
ccResult = processor.call();
}
//同步调用的返回结果,永不为null,默认为CCResult.defaultNullResult()
return ccResult;
}
}
ComponentManager.call(cc)方法的核心操作已经在注释了。
接下来看看ChainProcessor的call方法是什么操作:
public CCResult call() throws Exception {
//...
//从开始调用的时候就开始进行监控,也许时间设置的很短,可能都不需要执行拦截器调用链
CCMonitor.addMonitorFor(cc);
CCResult result;
try {
//开启一系列的拦截器操作
result = chain.proceed();
} finally {
CCMonitor.removeById(callId);
}
//...
return result;
}
可以看到首先将CC对象添加到一个监控中,然后开始启动拦截链的调用,也就是执行逐个添加进去的拦截器,最后将CC对象从监控中移除,关于监控的作用后边会讲到。
chain.proceed():开始拦截器链的链式调用,它会逐个调用,首先会调用用户设置的拦截器,其次是刚才添加进去的ValidateInterceptor。
ValidateInterceptor: 检查cc的合法性
判断是否是当前进程的组件,是的话添加LocalCCInterceptor
在本app(主进程和子进程)中找不到组件,则添加RemoteCCInterceptor
否则在app内部子进程的组件,添加SubProcessCCInterceptor
最后添加Wait4ResultInterceptor
public CCResult intercept(Chain chain) {
//省略一系列的检查:大致如下
/**
1. 没有指定要调用的组件名称,中止运行
2. context为null (没有设置context 且 CC中获取application失败)
3. 当前进程中不包含此组件,查看一下其它进程中是否包含此组件
4. 本app内所有进程均没有指定的组件,并且设置了不会调用外部app的组件
*/
//...
//执行完自定义拦截器,并且通过有效性校验后,再确定具体调用组件的方式
if (ComponentManager.hasComponent(componentName)) {
//调用当前进程中的组件
chain.addInterceptor(LocalCCInterceptor.getInstance());
} else {
if (notFoundInCurApp == null) {
notFoundInCurApp = TextUtils.isEmpty(ComponentManager.getComponentProcessName(componentName));
}
if (notFoundInCurApp) {
//调用设备上安装的其它app(组件单独运行的app)中的组件
chain.addInterceptor(RemoteCCInterceptor.getInstance());
} else {
//调用app内部子进程中的组件
chain.addInterceptor(SubProcessCCInterceptor.getInstance());
}
}
//添加最后的Wait4ResultInterceptor拦截器,用来等待结果的发送。
chain.addInterceptor(Wait4ResultInterceptor.getInstance());
// 继续拦截器链的调用
return chain.proceed();
}
LocalCCInterceptor: 判断是否需要在主线程执行,是的话放到主线程中,否则放到线程池中。
public CCResult intercept(Chain chain) {
CC cc = chain.getCC();
IComponent component = ComponentManager.getComponentByName(cc.getComponentName());
//...
if (runOnMainThread) {
//需要在主线程运行,但是当前线程不是主线程
ComponentManager.mainThread(runnable);
} else {
//需要在子线程运行,但当前线程不是子线程
ComponentManager.threadPool(runnable);
}
//...
if (!shouldSwitchThread) {
//不需要切换线程,直接运行
runnable.run();
}
//兼容以下情况:
// 1. 不需要切换线程,但需要等待异步实现调用CC.sendCCResult(...)
// 2. 需要切换线程,等待切换后的线程调用组件后调用CC.sendCCResult(...)
if (!cc.isFinished()) {
chain.proceed();
}
//...
return cc.getResult();
}
LocalCCRunnable的run方法:调用对应组件的onCall,也就是我们实现IComponent 的onCall方法。
public void run() {
//...
boolean callbackDelay = component.onCall(cc);
//...
}
接下来是跨进程调用组件的拦截器,先上一张来自文档网站的图片:
根据图片我们可以知道CC跨进程调用组件的过程是这样的:先在本地进程开始CC的调用,然后进到RemoteCCInterceptor,此拦截器开启跨进程的调用,然后在目标进程中同样开启CC的组件调用,最后再把结果原路返回,这种设计感觉很妙。
RemoteCCInterceptor:
public CCResult intercept(Chain chain) {
String processName = getProcessName(chain.getCC().getComponentName());
if (!TextUtils.isEmpty(processName)) {
return multiProcessCall(chain, processName, REMOTE_CONNECTIONS);
}
return CCResult.error(CCResult.CODE_ERROR_NO_COMPONENT_FOUND);
}
首先会获取组件对应的进程名,通过一个名字为REMOTE_CONNECTIONS的Map容器遍历获取,它专门用于存放包名与远程服务IRemoteCCService,IRemoteCCService有一个getComponentProcessName方法:可以通过组件名来获取对应的进程名,如果存在进程名,那么执行multiProcessCall方法:
CCResult multiProcessCall(Chain chain, String processName
, ConcurrentHashMap<String, IRemoteCCService> connectionCache) {
//......
ProcessCrossTask task = new ProcessCrossTask(cc, processName, connectionCache, isMainThreadSyncCall);
ComponentManager.threadPool(task);
//......
}
multiProcessCall方法核心逻辑为创建一个ProcessCrossTask,然后放到线程池中执行,ProcessCrossTask的run方法会将传输的参数封装成一个RemoteCC对象,然后进程跨进程传输:
private void call(RemoteCC remoteCC) {
try {
//通过缓存获取远程服务IRemoteCCService
service = connectionCache.get(processName);
if (service == null) {
//获取跨进程通信的binder
service = getMultiProcessService(processName);
if (service != null) {
//缓存起来
connectionCache.put(processName, service);
}
}
//...
//开启跨进程传输
service.call(remoteCC, new IRemoteCallback.Stub() {
public void callback(RemoteCCResult remoteCCResult) throws RemoteException {
setResult(remoteCCResult.toCCResult());
}
});
}
}
task创建RemoteCC对象,RemoteCC创建过程会将参数解析成Map对象,键为String、值为Object,底层采用json解析,所以跨进程传输参数时要检查传输的对象是否能够成功解析成json格式,然后ProcessCrossTask的call方法会获取IRemoteCCService(用于跨进程通信),最后执行call方法开启了跨进程的调用,并传输一个支持远程传输的对象做为回调。
RemoteCCService的call方法:
public void call(final RemoteCC remoteCC, final IRemoteCallback callback) throws RemoteException {
//...失效判断
String componentName = remoteCC.getComponentName();
final String callId = remoteCC.getCallId();
//...
final CC cc = CC.obtainBuilder(componentName)
.setActionName(remoteCC.getActionName())
.setParams(remoteCC.getParams())
.setCallId(remoteCC.getCallId())
.withoutGlobalInterceptor() //为了不重复调用拦截器,全局拦截器需要下沉复用,只在调用方进程中执行
.setNoTimeout() //超时逻辑在调用方进程中处理
.build();
if (remoteCC.isMainThreadSyncCall()) {
mainThreadHandler.post(new Runnable() {
public void run() {
CCResult ccResult = cc.call();
doCallback(callback, callId, ccResult);
}
});
} else {
cc.callAsync(new IComponentCallback() {
public void onResult(CC cc, CCResult result) {
doCallback(callback, callId, result);
}
});
}
}
RemoteCCService同样进行CC组件的调用,只是这里调用的将会是本地进程的组建了,然后将调用的结果都会通过doCallback方法进行处理,只是会分是否异步调用而已。
private static void doCallback(IRemoteCallback callback, String callId, CCResult ccResult) {
//......
remoteCCResult = new RemoteCCResult(ccResult);
//......
callback.callback(remoteCCResult);
//......
}
doCallback将结果封装在RemoteCCResult中,并且通过跨进程传输过来的IRemoteCallback回调,也就是回调到ProcessCrossTask中,然后调用setResult方法。最后调用的拦截器名字为Wait4ResultInterceptor。
Wait4ResultInterceptor:
public CCResult intercept(Chain chain) {
CC cc = chain.getCC();
cc.wait4Result();
return cc.getResult();
}
首先wait,在结果没发送前进行阻塞,用于等待调用CC.sendCCResult,它会调用cc的wait4Result。
void wait4Result() {
//等待调用CC.sendCCResult(callId, result)
synchronized (wait4resultLock) {
if (!isFinished()) {
try {
verboseLog(callId, "start waiting for CC.sendCCResult(...)");
waiting = true;
//先阻塞起来,等待CC.sendCCResult
wait4resultLock.wait();
verboseLog(callId, "end waiting for CC.sendCCResult(...)");
} catch (InterruptedException ignored) {
}
}
}
}
/ 开启支持进程调用 /
通过调用CC.enableRemoteCC开启支持远程调用,然后交由RemoteCCInterceptor。RemoteCCInterceptor的enableRemoteCC方法执行了两个操作,第一为监听设备上其它包含CC组件的app,第二是扫描设备上安装的可供跨app调用组件的app列表。
//监听设备上其他包含CC组件的app,通过广播进行监听
private void listenComponentApps() {
IntentFilter intentFilter = new IntentFilter();
//添加一系列的intentFilter关心的action,比如添加或者移除进程名
CC.getApplication().registerReceiver(new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String packageName = intent.getDataString();
//...
String action = intent.getAction();
if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
//移除远程进程的广播
REMOTE_CONNECTIONS.remove(packageName);
} else {
//检测组件App是否存在,并顺便唤醒App
if (RemoteConnection.tryWakeup(packageName)) {
ComponentManager.threadPool(new ConnectTask(packageName));
}
}
}
}, intentFilter);
}
这里边动态注册一个广播,这个广播用来监听远程进程包含CC组件的app,然后进行对应的移除或者添加操作,添加操作执行的是一个任务:ConnectTask,这个任务里边是获取一个远程服务IRemoteCCService,然后保存在REMOTE_CONNECTIONS中。
获取远程服务IRemoteCCService首先会通过packageName获取一个IRemoteCCService对象:
protected IRemoteCCService getMultiProcessService(String packageName) {
long start = System.currentTimeMillis();
IRemoteCCService service = null;
//会每50毫秒通过RemoteCCService获取一次,直到获取到
while (System.currentTimeMillis() - start < MAX_CONNECT_TIME_DURATION) {
service = RemoteCCService.get(packageName);
if (service != null) {
break;
}
SystemClock.sleep(50);
}
return service;
}
RemoteCCService获取IRemoteCCService的流程如下:
从缓存中查找与packageName对应的IRemoteCCService
查不到则进行一个同步操作,同步操作中开始还是会继续查缓存,防止在多线程的情况下创建多个
最后通过ContentProvider的方式去获取一个IRemoteCCService对象,此过程会先获取一个bundle对象,再通过bundle获取一个BinderWrapper,BinderWrapper对象获取binder,然后再通过远程传输的套路将binder转换成我们想要的IRemoteCCService,相信接触过跨进程通信的同学不会陌生,它里边也是有aidl文件生成的代码,源码如下:
public static IRemoteCCService getRemoteCCService(Cursor cursor) {
if (null == cursor) {
return null;
}
Bundle bundle = cursor.getExtras();
bundle.setClassLoader(BinderWrapper.class.getClassLoader());
BinderWrapper binderWrapper = bundle.getParcelable(KEY_BINDER_WRAPPER);
if (binderWrapper != null) {
IBinder binder = binderWrapper.getBinder();
return IRemoteCCService.Stub.asInterface(binder);
}
return null;
}
回到ConnectTask任务,它会将获取到的IRemoteCCService与packageName对应保存在REMOTE_CONNECTIONS中,这也是为什么上面所讲到的,通过packageName就可以获取IRemoteCCService。
开启远程传输的第二步时扫描当前app设备上安装的可供跨app调用组件的app列表,搜索是通过Intent(“action.com.billy.cc.connection”)对象去进行的。核心流程为:查询所有已经有Intent(“action.com.billy.cc.connection”)的应用程序,也就是有action为action.com.billy.cc.connection的activity,然后遍历添加对应的包名。核心代码如下:
public static List<String> scanComponentApps() {
//......
Intent intent = new Intent("action.com.billy.cc.connection");
List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
List<String> packageNames = new ArrayList<>();
for (ResolveInfo info : list) {
ActivityInfo activityInfo = info.activityInfo;
String packageName = activityInfo.packageName;
if (curPkg.equals(packageName)) {
continue;
}
if (tryWakeup(packageName)) {
packageNames.add(packageName);
}
}
return packageNames;
}
/ 发送结果 /
当我们在组件中执行完成时,就需要调用CC.sendCCResult来告诉调用方结果:
public static void sendCCResult(String callId, CCResult ccResult) {
CC cc = CCMonitor.getById(callId);
//...
cc.setResult4Waiting(ccResult);
//...
}
首先通过CCMonitor来获取cc对象,CCMonitor作用不小,后面会说一说它在整个过程执行了哪些操作。
获取到cc后,会调用setResult4Waiting来先setResult,最后解除wait,也就是解除刚刚在Wait4ResultInterceptor拦截器中引起的阻塞。这样调用方就可以获取到调用的结果。
在开始组件间调用时,CCMonitor.addMonitorFor(cc);将cc对象先放到CCMonitor,当执行结束,不管失败与否,会调用CCMonitor.removeById(callId);通过调用id来移除cc对象。CCMonitor的最重要的作用是监控cc对象,比如超时、或者所在activity销毁时的对应操作。
/ 总结 /
到这里基本对CC的源码流程基本过了一遍,它使用了责任链模式很值得我们学习,还有它的跨进程调用也很妙。在使用上,CC这种基于组件总线完美解决了组件化过程中渐进式的困难,所以当要对老项目进行组件化时,很推荐使用它。
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注
本文地址:https://blog.csdn.net/c10WTiybQ1Ye3/article/details/100135310