cc解读

addComponent

addComponent为CC依赖组件的方式,原理参考com.billy.android.register.cc.ProjectModuleManager#addComponentDependencyMethod,实际上是在各project.ext.addComponent来实现的。实际上addComponent会转换为api、compile依赖方式。
 
所有组件可直接在Android Studio中点击绿色Run按钮直接以app方式运行
  • 需要注意的是,编译主app时,需要在local.properties添加配置:module_name=true 将单独运行的组件从主app打包依赖项中排除
demo_component_a=true demo_component_b=true
在local.properties中增加以上两句表示这两个组件被排除宿主app,不参宿主打包相关逻辑参考以下代码(com.billy.android.register.cc.ProjectModuleManager#addComponentDependencyMethod)
static void addComponentDependencyMethod(Project project, Properties localProperties) { project.ext.addComponent = { dependencyName, realDependency = null -> ....... //配置组件为true时,将排除这个组件的模块依赖,不再参与宿主打包 def excludeModule = 'true' == localProperties.getProperty(dependencyName) if (!excludeModule) { def dependencyMode = GradleVersion.version(project.gradle.gradleVersion) >= GradleVersion.version('4.1') ? 'api' : 'compile' ....... } } }
 
 
cc-register本地发布
classpath "com.billy.android:cc-register:$ccRegisterVersion"
project.dependencies.add('api', "com.billy.android:cc:$ccVersion")
项目中依赖的cc-register、com.billy.android:cc均为maven依赖,尝试通过group来本地依赖失败,后续再研究原因
所以使用了本地打包来进行调试
//cc-register->bintray.gradle apply plugin: 'maven' uploadArchives { repositories { mavenDeployer { repository(url: uri('../repo-local')) //deploy到本地仓库 pom.groupId = publishedGroupId pom.artifactId = 'cc-register' pom.version = libraryVersion } } } //project的gradle buildscript { ext.libraryVersion = '1.1.2_test_1' repositories { maven{ url rootProject.file("repo-local") } } dependencies { classpath "com.billy.android:cc-register:$libraryVersion" } } allprojects { repositories { maven{ url rootProject.file("repo-local") } } }
注:
打包插件是一直失败,可能跟bintray.gradle文件有关,将bintray.gradle文件内uploadArchives相关内容拷贝至cc-register、cc的build.gradle即可打包完成
 
cc-register调试
cc-register做为一个编译期插件,我们可以针对project.apply plugin: 'cc-register'进行依赖该插件的module,我们可以通过gradle窗口进行各module的debug->demo_component_a:assembleDebug来进行调试。但这种方式实际上并没有执行相应module的assembleDebug,这给我们调试困扰,所以此时我们可以换一种方式进行调试
  • 创建remote任务->debug_cc_register_plugin
  • 执行gradlew :demo_component_a:assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true 命令,编译会启动demon进程进入挂起状态
  • 从IDE中选择debug_cc_register_plugin,点击调试(remote任务只支持调试)
  • 此时demo_component_a:assembleDebug命令继续执行,我们可以通过断点插件位置进行调试插件
 
com.android.application、com.android.library依赖解读
  • demo的gradle文件中ext.mainApp表明demo做为项目的application存在
  • 项目所包括的demo_component_a、demo_component_b、demo_base这些模块仅仅是通过apply from: rootProject.file('cc-settings-demo.gradle')方式依赖了统一这个gradle文件,并不能确认当前的module是以library还是以application方式运行。
  • 在cc-settings-demo.gradle中又依赖了project.apply plugin: 'cc-register'这个插件。实际上这些module的真实身份是在RegisterPlugin这个插件确认的。在编译阶段,通过我们执行的gradle命令来决定自已的身份
  • 当我们通过anddroid studio执行选择demo_component_a这个app运行时,实际也相当于执行了gradlew :demo_component_a:assembleDebug这样的命令。此时RegisterPlugin就会将demo_component_a做为application来运行(项目构建配置阶段,通过com.billy.android.register.cc.ProjectModuleManager#manageModule这个方法来动态改变了demo_component_a的身份,assembleFor=true时runAsApp也为true,会对当前project增加project.apply plugin: 'com.android.application'来确认当前moduel的application身份)
  • 通过runAsApp来动态决定当前的module为library或者application
为什么在android studio中demo、demo_component_a、demo_component_b都以app的形式存在?
当我们通过sync project同步这个cc项目时,我们观察这个构建过程,在Configure配置阶段,会执行RegisterPlugin插件的ProjectModuleManager.manageModule方法,
此时project.gradle.startParameter.taskNames获取到的taskNames为空,这也就导致taskIsAssemble为false,这也就会将runAsApp设置为true,所以这些module默认就以application的形式存在,这也是为什么每个module都可以直接在ide中看到并选择运行的原因
 
CC类
CC类是一次组件接口调用的载体类(data),承载接口调用的各种参数,调用结果,也可以理解为是一个装饰器类。
 
ObjPool对象池
ObjPool用来存储创建的Builder对象
当我们通过get访问对象时,优先从缓存中获取,如果缓存中没有实例,则创建一个实例并返回(创建实例时会调用builder的init方法)
get
1、当调用CC.obtainBuilder时,会调用BUILDER_POOL.get方法,优先尝试从队列里取出一个对象,队列里无对象时会通过newInstance创建一个对象,同时会调用这个对象的init,init方法会创建一个CC对象(CC对象以组件名称componentName为构造参数),但这个CC对象在调用Builder.build()方法时被清除掉(build方法中BUILDER_POOL.put最终调用ObjPool.put时,会通过reset将builder对象的cr清空),此时builder内的cr为空。但其build方法会返回CC对象,方便调用方使用CC对象。
put
将对象添加到对列,并通过reset方法将builder内部的cc对象清空
根据以上结论,虽然ObjPool内部使用了对列ConcurrentLinkedQueue,但一情况下,只会存储一个builder对象,每次通过CC.obtainBuilder都会反复使用这一个对象
 
cc.call
  • callId通过AtomicInteger生成一个不重复的自增唯一ID
  • 当调用call方法在主线程时,会设置超时时间timeout,当超过超时时间,任务会通过CCMonitor结束任务
  • ComponentManager.call为任务的入口方法,会创建一个chain责任链对象,添加拦截器LogInterceptor(通过AOP技术注册到GlobalCCInterceptorManager内,这个需要再深入研究下注册插件)、ValidateInterceptor
  • ComponentManager 类是CC 中的总线类,或者说仓库类。维护着CC 中的路由信息,路由信息存储在COMPONENTS内,这同样是AOP技术,需要再研究下
  • ChainProcessor是Chain的封装类,同时ChainProcessor也是一个继承Callable线程类,其call负责chain的proceed处理逻辑及其它如监控CCMonitor逻辑、异常逻辑、返回结果等逻辑处理
  • cc.isAsync根据仅当通过callAsync方式调用任务时会异步执行任务,此时会将ChainProcessor加入线程池执行;同步调用则直接调用processor.call()并将结果返回
 
CCMonitor
CC_MAP内存储所有需要监控的CC任务,key为cc.callId
在ChainProcessor的call执行任务开始时,会通过CCMonitor.addMonitorFor方法对cc的任务进行监控,默认任务超时时间2秒
addCancelOnFragmentDestroyIfSet通过注册FragmentLifecycleCallbacks,在onFragmentDestroyed通过匹配依附的fragment,对任务做取消cancel操作,取消操作实际上是将cc.canceled设为true、finished设为true,并通过setResult4Waiting方法通知任务完成
(当通过cancelOnDestroyWith方法设置fragment.onDestroy时自动cancel当前任务)
TimeoutMonitorThread为CC超时监控线程类,会检查所有CC任务,LOCK.wait先等待minTimeoutAt(minTimeoutAt为所有监控任务的最小等待时间),然后遍历所有cc任务,针对未完成但超时的任务通过executeTimeout来进行超时处理(为了方便调试,目前只有非调试模式支持取消)
,针对需要依赖fragment取消的任务,可以通过FragmentMonitor进行管理
cancelOnDestroyWith,设置activity.onDestroy时自动cancel;ActivityMonitor可以监控所有Activity的销毁过程,在onActivityDestroyed内,匹配到CC任务依赖的Activity时做任务取消的操作
当ChainProcessor的call方法执行完成后,会通过CCMonitor.removeById(callId)方法取消监控
 
拦截器
Chain在初始化时默认会先添加LogInterceptor、ValidateInterceptor这两个拦截器
在执行proceed方法时先顺序执行这两个拦截器,在执行到ValidateInterceptor时,通过ComponentManager.hasComponent检查目标组件是否存在,存在时会继续添加LocalCCInterceptor拦截器,最后增加Wait4ResultInterceptor拦截器
LocalCCInterceptor解析
cc = CC.obtainBuilder(COMPONENT_NAME_A) .setActionName("showActivityA") .build(); result = cc.call();
如上代码,我们调用COMPONENT_NAME_A的showActivityA方法,实际上是通过LocalCCInterceptor来完成的,在LocalCCInterceptor,先通过ComponentManager.getComponentByName获取到组件COMPONENT_NAME_A的实现类ComponentA,通过LocalCCRunnable来完成任务的调用,执行showActivityA方法,实际上对应的是openActivity方法,其内部打开了ActivityA,并CC.sendCCResult来设置调用成功信息,并通过setResult4Waiting来设置异步回调,此是被wait锁住的wait4resultLock被唤醒,Wait4ResultInterceptor做为最后一个拦截器执行完成,将执行result一层一层往上返回
 
CC注册cc-register
组件映射关系的收集是通过gradle插件 cc-register 来完成的
cc作者单独开放AutoRegister库https://github.com/luckybilly/AutoRegister,和cc-register逻辑基本一致,名词解释如下
scanInterface : (必须)字符串,接口名(完整类名),所有直接实现此接口的类将会被收集
codeInsertToClassName : (必须)字符串,类名(完整类名),通过编译时生成代码的方式将收集到的类注册到此类的codeInsertToMethodName方法中
codeInsertToMethodName: 字符串,方法名,注册代码将插入到此方法中。若未指定,则默认为static块,(方法名为:)
registerMethodName : (必须)字符串,方法名,静态方法,方法的参数为 scanInterface
scanSuperClasses : 字符串或字符串数组,类名(完整类名),所有直接继承此类的子类将会被收集
include : 数组,需要扫描的类(正则表达式,包分隔符用/代替,如: com/billy/android/.),默认为所有的类exclude : 数组,不需要扫描的类(正则表达式,包分隔符用/代替,如: com/billy/android/.),
自动注册的实现原理
  • gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件
  • 在Tansform中收集相关类,编译期间修改字节码,加入到管理类中
  • 当我们项目的各模模块依赖了com.billy.android:cc:ccVersion、project.apply plugin: 'cc-register'这两个时,会通过cc-register完成对CC的自注册
  • 通过sh jadx-gui classes.dex观察cc的demo-debug.apk中的classes.dex文件
 
当我们项目的各模模块依赖了com.billy.android:cc:ccVersion、project.apply plugin: 'cc-register'这两个时,会通过cc-register完成对CC的自注册
通过sh jadx-gui classes.dex观察cc的demo-debug.apk中的classes.dex文件
  • 发现ComponentManager内被自动注入了注册的代码,代码如下
ComponentManager类中 static { registerComponent(new DynamicComponentOption()); registerComponent(new ComponentA()); registerComponent(new ComponentB()); registerComponent(new LifecycleComponent()); registerComponent(new JsBridgeComponent()); registerComponent(new WebComponent()); }
项目中各模块继承了IComponent接口的类被自动注册到了ComponentManager的COMPONENTS中
  • GlobalCCInterceptorManager完成对全局拦截器的注册,默认CC是没有任何拦截器的,我们的项目里自定义了一个LogInterceptor,编译期将这个LogInterceptor注册到了GlobalCCInterceptorManager--->INTERCEPTORS
GlobalCCInterceptorManager类中 static { registerGlobalInterceptor(new LogInterceptor()); }
  • RemoteParamUtil增加了对json转换实现类的注册
RemoteParamUtil类中 static { initRemoteCCParamJsonConverter(new GsonParamConverter()); }
以上所有注册类逻辑参考com.billy.android.register.cc.DefaultRegistryHelper(codeInsertToMethodName为空时默认插入到static方法中)
  • 在cc-settings-demo.gradle中,增加了自定义注册代码,代码如下
ccregister.registerInfo.add([ //在自动注册组件的基础上增加:自动注册组件B的processor 'scanInterface' : 'com.billy.cc.demo.component.b.processor.IActionProcessor' , 'codeInsertToClassName' : 'com.billy.cc.demo.component.b.ComponentB' , 'codeInsertToMethodName' : 'initProcessors' , 'registerMethodName' : 'add' ])
注册代码最终目的为在com.billy.cc.demo.component.b.ComponentB类中的initProcessors方法内增加注册代码,代码如下
private void initProcessors() {//将所有实现IActionProcessor接口的类通过add方法添加到ComponentB的HashMap<String, IActionProcessor>中 add(new LoginProcessor()); add(new LoginObserverAddProcessor()); add(new ShowActivityProcessor()); add(new LoginObserverRemoveProcessor()); add(new GetDataProcessor()); add(new GetNetworkDataProcessor()); add(new LogoutProcessor()); add(new GetLoginUserProcessor()); add(new CheckAndLoginProcessor()); }
以上代码插入逻辑参考com.billy.android.register.RegisterExtension#convertConfig
  • 核心插入代码逻辑参考(代码块来自于RegisterTransform)
CodeScanner scanProcessor = new CodeScanner(extension.list, cacheMap) // 遍历输入文件 inputs.each { TransformInput input -> // 遍历jar input.jarInputs.each { JarInput jarInput -> if (jarInput.status != Status.NOTCHANGED && cacheMap) { cacheMap.remove(jarInput.file.absolutePath) } scanJar(jarInput, outputProvider, scanProcessor) } // 遍历目录 input.directoryInputs.each { DirectoryInput directoryInput -> long dirTime = System.currentTimeMillis() def root = scanClass(outputProvider, directoryInput, scanProcessor) long scanTime = System.currentTimeMillis() println "${PLUGIN_NAME} cost time: ${System.currentTimeMillis() - dirTime}, scan time: ${scanTime - dirTime}. path=${root}" } }
 
queryIntentActivities解读
  • 在RemoteConnection中通过queryIntentActivities获取了Intent=action.com.billy.cc.connection所有已安装应用的app列表
  • 假设我们安装com.billy.cc.demo、com.billy.cc.demo.component.jsbridge这两个app,返回的列表如下
ResolveInfo{67fc25c com.billy.cc.demo/com.billy.cc.core.component.remote.RemoteConnectionActivity m=0x108000}
ResolveInfo{c5f965 com.billy.cc.demo.component.jsbridge/com.billy.cc.core.component.remote.RemoteConnectionActivity m=0x108000}
为什么可以获取以上两个app?
com.billy.cc.demo、com.billy.cc.demo.component.jsbridge这两个app都通过cc-settings-demo.gradle依赖了com.billy.android:cc:$ccVersion"这个库。也就是说这两个app里都存在CC库中定义的com.billy.cc.core.component.remote.RemoteConnectionActivity这样一个activity,所以我们可以通过queryIntentActivities来获取所有依赖了com.billy.cc.core.component.remote.RemoteConnectionActivity的应用
 
子进程组件调用(需要补充)
  • 针对子进程的组件,可以通过在组类(实现IComponent的类)上通过SubProcess注解标记当前组为子进程(参考WebComponent)
  • ComponentManager.COMPONENTS只存储当前进程的组件(ComponentManager.registerComponent时非当前进程组件不会添加到COMPONENTS内),COMPONENT_PROCESS_NAMES内会存储所有进程的组件,在进行组件调用时会先经过ValidateInterceptor拦截器,
  • 在ValidateInterceptor中,当确认当前APP的主进程没有这个组件任务而子进程又包含这个组件时,会增加SubProcessCCInterceptor拦截器
  • 在SubProcessCCInterceptor中,有一个ProcessCrossTask线程任务,会通过ComponentManager.CC_THREAD_POOL执行线程任务
 
enableRemoteCC(CC.enableRemoteCC)
  • 在MyApp中通过CC.enableRemoteCC(true)设置了允许CC进行跨进程远程组件访问,同时这里也对跨进程组件进行了一系列的初始化
  • listenComponentApp用户注册监听安装、卸载、覆盖应用时,唤醒应用进程,并通过ConnectTask缓存应用的binder对象到REMOTE_CONNECTIONS
  • scanComponentApps获取当前设备上安装的可供跨app调用组件的App列表,参考依据是所有注册action.com.billy.cc.connection的activity的应用(因为CC的组件应用都依赖了com.billy.android:cc:$ccVersion"这个库,所以都会注册这个action)
  • 通过ConnectTask缓存可用于跨进程通信的应用binder对象到REMOTE_CONNECTIONS
  • RemoteConnection.tryWakeup,通过启动组件内的RemoteConnectionActivity来唤醒组件App(如com.billy.cc.demo.component.jsbridge应用进程就是在此处被唤配的)
 
组件间通信解读
组件间通信分为两种情况,一种是多个组件打包成一个APP,这种情况下通过注册ComponentManager来管理多个主件,并通过拦截器调用,最完完成组件的调用
另一种是组件单远运行的app,宿主可以和单独运行的组件app进行通信。这部分逻辑可以参考RemoteCCInterceptor
RemoteCCInterceptor解读
  • RemoteCCInterceptor.REMOTE_CONNECTIONS存储了跨进程访问的链接对象
  • ConnectTask,通过provider来获取组件进程的RemoteCursor对象cursor,RemoteCursor内的Bundle对象binderExtras存储了组件进程的RemoteCCService,RemoteCCService为跨进程调用组件的Binder类(IRemoteCCService的aidl文件的IRemoteCCService.Stub子类)
  • IRemoteCCService.getService,getDispatcherProviderUri获取的uri如content://com.billy.cc.demo.component.jsbridge.com.billy.cc.core.remote/cc,这其实是访问包名为com.billy.cc.demo.component.jsbridge的demo_component_jsbridge组件的provider,观察demo_component_jsbridge打包的apk的mainfest.xml文件内provider的定义如下
<provider android:name="com.billy.cc.core.component.remote.RemoteProvider" android:exported="true" android:authorities="com.billy.cc.demo.component.jsbridge.com.billy.cc.core.remote" />
如上provider定义,CC.getApplication().getContentResolver().query("content://com.billy.cc.demo.component.jsbridge.com.billy.cc.core.remote/cc")即获取到了demo_component_jsbridge的provider--->RemoteProvider,这即达到了跨进程访问的第一步
  • RemoteCursor.getRemoteCCService返回了组件进程的binder对象,宿主的将binder对象存储在RemoteCCService.CACHE。此是我们即获取了组件进程的binder对象,后续对组件进程的访问都会通过这个binder对象来完成
  • RemoteCCService重写了call方法,这也是跨进程调用最关键的一个方法,RemoteCC、IRemoteCallback为跨进程调用时实体对象,和我们APP应用中的bean一样。
Android使用AIDL传递对象
  • 由于跨进程访问无法确认上的组件app的状态(可能随时被杀掉),所以每次跨组件访问都是一个短暂的访问,虽然在宿主demo启动时,RemoteCCInterceptor.REMOTE_CONNECTIONS存储了组件app的binder对象,但在实际的跨进程调用时,当通过跨进程的binder对象访问失败时,仍然会重新获取组件APP进程的binder对象,这也就保证了不管组件app是否存活始终可以调用成功的目的。参考代码
private String getProcessName(String componentName) { String processName = null; try { for (Map.Entry<String, IRemoteCCService> entry : REMOTE_CONNECTIONS.entrySet()) { try { processName = entry.getValue().getComponentProcessName(componentName);//先用组件对象的binder对象直接进行跨进程调用 } catch(DeadObjectException e) { String processNameTo = entry.getKey(); RemoteCCService.remove(processNameTo); IRemoteCCService service = RemoteCCService.get(processNameTo);//当调用失败时,会重新获取组件的binder对象,并进行缓存 if (service == null) { String packageName = processNameTo.split(":")[0]; boolean wakeup = RemoteConnection.tryWakeup(packageName); CC.log("wakeup remote app '%s'. success=%b.", packageName, wakeup); if (wakeup) { service = getMultiProcessService(processNameTo); } } if (service != null) { try { processName = service.getComponentProcessName(componentName); REMOTE_CONNECTIONS.put(processNameTo, service);//再次缓存组件进程的binder对象 } catch(Exception ex) { CCUtil.printStackTrace(ex); } } } } } catch(Exception e) { CCUtil.printStackTrace(e); } return processName; }
  • 我们通过在local.properties中增加以下代码,将这两个组件排除,宿主app打包apk将不再包括这两个组件,此时当在宿主调用demo_component_a的某一个方法时,通过拦截器到ValidateInterceptor时,由于宿主ComponentManager.hasComponent未从apk内找到demo_component_a组件,此时会责任链中会添加RemoteCCInterceptor做跨进程调用。
demo_component_a=true demo_component_b=true
跨进程调用,需要将demo_component_a以apk方式安装才可以调用成功;当未以单独apk安装demo_component_a时,跨进程调用会失败,代码参考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);//此时相当于processName为空,即未找到目标组件的应用,返回调用错误 }
 
 
 
相关链接
CC 原理简单分析
 
 
 

posted on 2022-03-26 22:59  neil-zhao  阅读(238)  评论(0编辑  收藏  举报

导航