使用方法
- 确保local配置正常 命令行执行hostnew、crm将两个app分别打包安装
- 在主App进行登录操作,然后打开CRM即可进行调试(需要跨组件调用时请使用CC进行调用)
I 准备
1.1 组件化
简单来说,就是将一个APP的业务功能进行拆分,每一个功能都是一个单独的工程,每个工程都能独立运行,且只包含自己的业务,我们姑且叫这个独立的功能为一个组件服务,最后整个APP由多个拆分出的组件集成而成。
1.2 组件化解决了哪些痛点
- 提高工程编译速度
进行组件化拆分后,每个业务或者功能都是一个单独的工程,这个单独的工程可以独立编译运行.。- 业务模块解耦
业务组件之间不能相互引用,每个组件都把对应的业务功能收敛在一个工程里,彼此互不打扰。- 提供单独的组件服务
1.2 框架CC
当ComponentMananger接收到组件的调用请求时,查找当前app内组件清单中是否含有当前需要调用的组件
-
有: 执行App内部CC调用的流程
-
没有:执行App之间CC调用的流程
II 独立编译CRM,都做了哪些工作?
2.1 应用初始化
- 增加全量可用的fsinit模块 在该lib中进行了App的基本初始化操作
- 新增HostFunction用于基础模块的获取
并在com.fxiaoke.host.App中对HostFunction进行初始化 - 将HostInterfaceManager中的部分方法转接到HostFunction中
2.2 登录信息
- 通过在清单文件中添加 Android:sharedUserId = ""属性来使得主App的登录cookie可以与CRM组件App共享
- 我们可以通过一个包名来得到对应的Context的全局变量,可以直接使用Context的一个静态方法:createPackageContext() 去访问另外一个app的代码和资源等信息
- 在FSContextManager中:登录注销保存的context由HostFunction.getInstance().getApp()获取
2.2.1 Android:sharedUserId = " "属性
在需要共享资源的项目的每个AndroidMainfest.xml中添加shareuserId的标签。
Android给每个APK进程分配一个单独的空间,manifest中的userid就是对应一个分配的Linux用户ID,并且为它创建一个沙箱,以防止影响其他应用程序(或者其他应用程序影响它)。用户ID 在应用程序安装到设备中时被分配,并且在这个设备中保持它的永久性。
通常,不同的APK会具有不同的userId,因此运行时属于不同的进程中,而不同进程中的资源是不共享的,保障了程序运行的稳定。
通过SharedUserId,拥有相同UserId的多个Apk可以配置成运行在同一个进程中.所以默认就是可以互相访问任意数据.
但是
因为sharedUserId属性的添加引出的问题:sharedUserId属性不同的应用不能覆盖安装
所以采用gradle修改清单文件的方式在App整体编译时(local文件中没有设置moduleName=true时)移除sharedUserId属性
清单文件合并后的地址:
fs-android\host\host_main\build\intermediates\manifests\full\online\debug\AndroidManifest.xml
2.3 如何独立运行crm
- 确保主app/build.gradle中是使用addComponent(参照)来添加对组件module的依赖
- 在local.properties中增加一行配置module_name=true 例如:crm=ture 表示主工程打包时将排除crm组件
注意 : 框架在接收到组件调用请求时,优先查看当前App内是否有本次CC调用指定的组件(参照3.5.1)
- 有: 执行App内部组件调用流程
- 没有:检查当前app是否开启跨app调用功能:
- 未开启: 返回-5的状态码,代表未找到指定的组件
- 已开启: 执行跨App组件调用流程
III 如何使用ComponentCaller建立一个新的组件
简而言之:我们通过CC将业务请求转发,在被调用的组件中处理请求
eg:在CRM组件中调用BI的页面 如:数据看板
- 步骤1:添加引用
- 步骤2:在CRM组件中原来调用startActivity(或发送请求的地方 对应类为BIGoPageImpl)进行调用
- 步骤3:在plugs/bi组件中实现一个继承自IComponent接口的类用来处理调用请求 (实际的context.startActivity(intent)也就是跳转工作在onCall中进行)
- 步骤4:在主app module中添加对所有组件module的依赖
基本用法
下面介绍在Android Studio中进行集成的详细步骤
3.1.2 在每个组件module(包括主app)的build.gradle中添加对CC框架的依赖并应用CC自动注册插件
-
使用公共文件的方式
下载或复制cc-settings-2.gradle文件放到工程的根目录下,并按如下方式添加修改module的build.gradle
这样做的好处是:以后可以在此文件中添加的配置可对所有组件module都生效
// apply plugin: 'com.android.library' 注释掉
添加 : apply from: rootProject.file(cc-settings-2.gradle)
//注意:最好放在build.gradle中代码的第一行
3.1.3 修改组件module的build.gradle,将applicationId去除或者按以下方式修改,否则在集成打包时会报错
android {
defaultConfig {
//仅在以application方式编译时才添加applicationId属性
if (project.ext.runAsApp) {
applicationId 'com.facishare.fs'
}
//...
}
//...
}
3.2. 在组件中创建实现IComponent接口的组件操作类(以CRM组件跳转BI页面为例)
- 创建组件(实现IComponen接口,需要保留无参构造方法)
- com.fxiaoke.plugin.bi.IComponent.BIHomeActivityComponent
- 请详细阅读注释
public class BIHomeActivityComponent implements IComponent {
/**
* BI报表详情页参数
*/
public static final String IS_FROM_DATA_CONTAINER = "getFromDataContainer";
public static final String VIEW_URL_KEY_IN_DATA_CONTAINER = "RptDetailWebAct_viewUrl";
@Override
public String getName() {
//此处return值为自己定义的标识符 确保与调用处CC.obtainBuilder("Component.BIActivityComponent")中的值保持一致
return "Component.BIActivityComponent";
}
@Override
public boolean onCall(CC cc) {
//actionName:构造CC对象时调用的setActionName 用于区分执行操作,非必需
String actionName = cc.getActionName();
switch (actionName) {
case "BIHomeActivity":
showBIHomeActivity(cc);
break;
case "DataBoardHomeAct":
showDataBoardHomeAct(cc);
break;
case "RptDetailWebAct":
showRptDetailWebAct(cc);
break;
//要确保任何分支case都有sendCCresult的调用!!
//下方示例:
default:
CC.sendCCResult(cc.getCallId(), CCResult.error("unsupported action name:" + actionName));
break;
}
//返回值说明
// false: 组件同步实现(onCall方法执行完之前会将执行结果CCResult发送给CC)
// true: 组件异步实现(onCall方法执行完之后再将CCResult发送给CC,CC会持续等待组件调用CC.sendCCResult发送的结果,直至超时)
return false;
}
private void showBIHomeActivity(CC cc) {
Context context = cc.getContext();
Intent intent = new Intent(context, BIHomeActivity.class);
if (!(context instanceof Activity)) {
//调用方没有设置context或app间组件跳转,context为application
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
//实际的Activity跳转操作
context.startActivity(intent);
//发送组件调用的结果(返回结果)
CC.sendCCResult(cc.getCallId(), CCResult.success());
}
3.3. 调用组件
- com.fxiaoke.dataimpl.bi.BIGoPageImpl
public class BIGoPageImpl implements IBIGoPage {
/**
* BI报表详情页参数
*/
public static final String IS_FROM_DATA_CONTAINER = "getFromDataContainer";
public static final String VIEW_URL_KEY_IN_DATA_CONTAINER = "RptDetailWebAct_viewUrl";
@Override
public void go2BIHome(Activity act) {
//在构建CC时可以通过addParam(String key, Object value)或addParams(Map<String, Object> params)携带参数
CCResult result1 =
CC.obtainBuilder("Component.BIActivityComponent")//根据步骤2中getName方法的返回值确定Icomponent
.setActionName("BIHomeActivity")
.build()
.call();
}
@Override
public void go2BIDataBoard(Activity act) {
CCResult result2 = CC.obtainBuilder("Component.BIActivityComponent")
.setActionName("DataBoardHomeAct")
.build()
.call();
}
@Override
public void go2BIRptDetailWebAct(Activity act, String categoryID, String loadUrl) {
//url可能很大,超过intent data size 限制
CCResult result3 = CC.obtainBuilder("Component.BIActivityComponent")
.setActionName("RptDetailWebAct")
.addParam("loadUrl", loadUrl)
.build()
.call();
}
}
//所有的请求api
//同步调用,直接返回结果
CCResult result = CC.obtainBuilder("ComponentA").build().call();
//或 异步调用,不需要回调结果
String callId = CC.obtainBuilder("ComponentA").build().callAsync();
//或 异步调用,在子线程执行回调
String callId = CC.obtainBuilder("ComponentA").build().callAsync(new IComponentCallback(){...});
//或 异步调用,在主线程执行回调
String callId = CC.obtainBuilder("ComponentA").build().callAsyncCallbackOnMainThread(new IComponentCallback(){...});
3.4. 在主app module中按如下方式添加对所有组件module的依赖
注意:组件之间不要互相依赖
ext.mainApp = true
apply from: rootProject.file(cc-settings-2.gradle)
//...
dependencies {
addComponent 'demo_component_a' //会默认添加依赖:project(':demo_component_a')
addComponent 'demo_component_kt', project(':demo_component_kt') //module方式
addComponent 'demo_component_b', 'com.billy.demo:demo_b:1.1.0' //maven方式
}
- 组件切换library和application方式编译时,只需在local.properties中进行设置,不需要修改app module中的依赖列表
- 运行主app module时会自动将【设置为以app方式编译的组件module】从依赖列表中排除
3.5 注意
3.5.1. CC会优先调用app内部的组件,只有在内部找不到对应组件且设置了CC.enableRemoteCC(true)
时才会尝试进行跨app组件调用。
所以,单组件以app运行调试时,如果主app要主动与此组件进行通信,要确保主app中没有包含此组件,做法为:
在工程根目录的local.properties
中添加如下配置,并重新打包运行主app
module_name=true #module_name为具体每个module的名称,设置为true代表以application方式编译调试,从主app中排除
示例:crm=true
3.5.2. 组件module可以直接点击android studio上的绿色Run按钮将组件作为app安装到手机上运行进行调试,可通过跨app调用组件的方式调用到主app内的所有组件
排除错误
-
一些常见的Crash:
你定义了AndroidManifest.xml了吗:CC的流程没跑通。常见错误:CC的接口不正确&&相应的组件模块没有applyCC-settings 尝试依照CC的日志进行错误排查]
空指针:将写在HostInterfaceManager的方法补充在HostFunction中 并尝试将其替换成HostFunction
- 先打开CC的日志开关,看日志中的返回码
CC.enableDebug(true); //普通调试日志,会提示一些错误信息
CC.enableVerboseLog(true); //组件调用的详细过程日志,用于跟踪整个调用过程
状态码清单
状态码 | 说明 |
---|---|
0 | CC调用成功 |
1 | CC调用成功,但业务逻辑判定为失败 |
-1 | 保留状态码:默认的请求错误code |
-2 | 没有指定组件名称 //考虑步骤4 留意编译过程中的log cc-register是否检测到实现IComponent接口的类 |
-3 | result不该为null。例如:组件回调时使用 CC.sendCCResult(callId, null) 或 interceptor返回null |
-4 | 调用过程中出现exception //请查看logcat |
-5 | 指定的ComponentName没有找到 //请检查obtainBuilder中传递的name与待调用组件中的GetName返回值是否相同 |
-6 | context为null,获取application失败,出现这种情况可以用CC.init(application)来初始化 |
-8 | 已取消 |
-9 | 已超时 |
-10 | component.onCall(cc) return false, 未调用CC.sendCCResult(callId, ccResult)方法 |
-11 | 跨app组件调用时对象传输出错,可能是自定义类型没有共用,//不能同时调用多个addParam |
常用操作
-
正式上线时禁用跨app调用组件
可在主app的application.onCreate中添加如下代码来禁用对跨app组件调用的支持
//跨app调用支持:debug时启用,release时禁用 CC.enableRemoteCC(BuildConfig.DEBUG);
-
开启/关闭CC调试日志
CC.enableDebug(trueOrFalse);
-
开启/关闭CC调用执行过程的跟踪日志
CC.enableVerboseLog(trueOrFalse);
-
在组件中返回CCResult
//设置成功的返回信息 CCResult ccResult = CCResult.success(key1, value1).addData(key2, value2); //设置失败的返回信息 CCResult ccResult = CCResult.error(message).addData(key, value); //发送结果给调用方 CC.sendCCResult(cc.getCallId(), ccResult)
-
发起链式调用时的参数设置
以下示例代码中的callAsync方法(异步调用)可以换成call方法(同步调用)
//设置Context信息 CC.obtainBuilder("ComponentA")...setContext(context)...build().callAsync() //关联Activity的生命周期,在onDestroy方法调用后自动执行cancel CC.obtainBuilder("ComponentA")...cancelOnDestroyWith(activity)...build().callAsync() //关联v4包Fragment的生命周期,在onDestroy方法调用后自动执行cancel CC.obtainBuilder("ComponentA")...cancelOnDestroyWith(fragment)...build().callAsync() //设置ActionName CC.obtainBuilder("ComponentA")...setActionName(actionName)...build().callAsync() //超时时间设置 CC.obtainBuilder("ComponentA")...setTimeout(1000)...build().callAsync() //参数传递 CC.obtainBuilder("ComponentA")...addParam("name", "billy").addParam("id", 12345)...build().callAsync() //添加拦截器 CC.obtainBuilder("ComponentA")...addInterceptor(new MyInterceptor())...build().callAsync()
-
解析组件调用的结果:CCResult
//读取调用成功与否 ccResult.isSuccess() //读取调用状态码(状态码对应的说明见README中的状态码清单) ccResult.getCode() //读取调用错误信息 ccResult.getErrorMessage() //读取返回的附加信息 Map<String, Object> data = ccResult.getDataMap(); if (data != null) { Object value = data.get(key) } // 根据key从map中获取内容的便捷方式(自动完成类型转换,若key不存在则返回null): User user = ccResult.getDataItem(key); //读取CCResult.data中的item