为了将CRM组件独立编译,我们都做了什么

使用方法

  • 确保local配置正常 命令行执行hostnew、crm将两个app分别打包安装
  • 在主App进行登录操作,然后打开CRM即可进行调试(需要跨组件调用时请使用CC进行调用)

I 准备

1.1 组件化

简单来说,就是将一个APP的业务功能进行拆分,每一个功能都是一个单独的工程,每个工程都能独立运行,且只包含自己的业务,我们姑且叫这个独立的功能为一个组件服务,最后整个APP由多个拆分出的组件集成而成。

1.2 组件化解决了哪些痛点

  • 提高工程编译速度
    进行组件化拆分后,每个业务或者功能都是一个单独的工程,这个单独的工程可以独立编译运行.。
  • 业务模块解耦
    业务组件之间不能相互引用,每个组件都把对应的业务功能收敛在一个工程里,彼此互不打扰。
  • 提供单独的组件服务

1.2 框架CC

  • 当ComponentMananger接收到组件的调用请求时,查找当前app内组件清单中是否含有当前需要调用的组件

  • 有: 执行App内部CC调用的流程


    image.png
  • 没有:执行App之间CC调用的流程


    image.png

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
    1

你定义了AndroidManifest.xml了吗:CC的流程没跑通。常见错误:CC的接口不正确&&相应的组件模块没有applyCC-settings 尝试依照CC的日志进行错误排查]

2

空指针:将写在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
    
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容