作者简介

本篇来自 lucky_billy 的投稿,分享了他的开源组件化框架,详细地讲解框架形成的思路,希望对大家有所帮助。

lucky_billy 的博客地址:

http://blog.csdn.net/cdecde111

解读开源框架设计思想

B站学习视频

满满诚意:【实战MVVM和Jetpack的完美结合让页面开发不再烦恼】
【1.什么是插件化】
【2.插件化能解决的问题及与组件化的区别】
【3.常用插件化框架对比】



【13.手写实现插件的资源加载】

点击【设计思想解读开源框架】学习笔记,学习路线获取!

前言

CC:Component Caller,一个android组件化开发框架, 已开源,github 地址:

https://github.com/luckybilly/CC

本文主要讲解框架实现原理,如果只是想了解一下如何使用,可直接到 github上查看README文档

首先说明一下,本文将讲述的组件化与业内的插件化(如:Atlas, RePlugin等)不是同一个概念

[图片上传失败...(image-cd3f7b-1607780116731)]

组件化开发:就是将一个app分成多个Module,每个Module都是一个组件(也可以是一个基础库供组件依赖),开发的过程中我们可以单独调试部分组件,组件间不需要互相依赖,但可以相互调用,最终发布的时候所有组件以lib的形式被主app工程依赖并打包成1个apk。

插件化开发:和组件化开发略有不用,插件化开发时将整个app拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk(或其他格式)分开或者联合打包。

本文将主要就以下几个方面进行介绍:

一、为什么需要组件化?

二、CC的功能介绍

三、CC技术要点

四、CC执行流程详细解析

为什么需要组件化?

关于使用组件化的理由,上网能搜到很多,如业务隔离、单独以app运行能提高开发及调试效率等等这里就不多重复了,我补充一条:组件化之后,我们能很容易地实现一些组件层面的AOP,例如:

  • 轻易实现页面数据(网络请求、I/O、数据库查询等)预加载的功能组件被调用时,进行页面跳转的同时异步执行这些耗时逻辑页面跳转并初始化完成后,再将这些提前加载好的数据展示出来
  • 在组件功能调用时进行登录状态校验
  • 借助拦截器机制,可以动态给组件功能调用添加不同的中间处理逻辑

CC的功能介绍

  1. 支持组件间相互调用(不只是Activity跳转,支持任意指令的调用/回调)
  2. 支持组件调用与Activity、Fragment的生命周期关联
  3. 支持app间跨进程的组件调用(组件开发/调试时可单独作为app运行)在独立运行组件时非常有用,比如:一个组件的某个功能要用到用户的登录信息,若未登录则调起登录组件的登录页面,若已登录则获取当前用户信息。此时可以直接使用主app中的登录组件及用户在主app中的登录状态,该组件作为app独立运行时无需依赖登录组件。
  4. 支持app间调用的开关及权限设置(满足不同级别的安全需求,默认打开状态且不需要权限)
  5. 支持同步/异步方式调用
  6. 支持同步/异步方式实现组件
  7. 调用方式不受实现方式的限制(例如:可以异步调用另一个组件的同步实现功能。注:不要在主线程同步调用耗时操作)
  8. 支持添加自定义拦截器(按添加的先后顺序执行)
  9. 支持超时设置
  10. 支持手动取消
  11. 编译时自动注册组件(IComponent),无需手动维护组件注册表(使用ASM修改字节码的方式实现)
  12. 支持动态注册/反注册组件(IDynamicComponent)
  13. 支持组件间传递Fragment等非基础类型的对象(组件在同一个app内时支持、跨app传递非基础类型的对象暂不支持)
  14. 尽可能的解决了使用姿势不正确导致的crash:组件调用处、回调处、组件实现处的crash全部在框架内部catch住同步返回或异步回调的CCResult对象一定不为null,避免空指针

demo效果演示

组件A 打包在主app中,组件B 为单独运行的组件app,下图演示了在主app中调用两者的效果,并将结果以Json的格式显示在下方:

CC技术要点

实现CC组件化开发框架主要需要解决的问题有以下几个方面:

  • 组件如何自动注册?
  • 如何兼容同步/异步方式调用组件?
  • 如何兼容同步/异步方式实现组件?
  • 如何进行跨进程组件任意功能的调用(不只是启动Activity)?
  • 组件如何更方便地在application和library之间切换?
  • 如何实现startActivityForResult?
  • 如何阻止非法的外部调用?
  • 如何与Activity、Fragment的生命周期关联起来

组件如何自动注册?

为了减少后期维护成本,想要实现的效果是:当需要添加某个组件到app时,只需要在gradle中添加一下对这个 module 的依赖即可(通常都是maven依赖,也可以是project依赖)

最初想要使用的是 annotationProcessor 通过编译时注解动态生成组件映射表代码的方式来实现。但尝试过后发现行不通,因为编译时注解的特性只在源码编译时生效,无法扫描到aar包里的注解(project依赖、maven依赖均无效),也就是说必须每个module编译时生成自己的代码,然后要想办法将这些分散在各aar种的类找出来进行集中注册。

ARouter 的解决方案是:

  • 每个 module 都生成自己的 java类,这些类的包名都是 ’com.alibaba.android.arouter.routes’
  • 然后在运行时通过读取每个 dex 文件中的这个包下的所有类通过反射来完成映射表的注册,详见 ClassUtils.java 源码运行时通过读取所有 dex 文件遍历每个 entry 查找指定包内的所有类名,然后反射获取类对象。这种效率看起来并不高。

ActivityRouter 的解决方案是(demo中有2个组件名为’app’和’sdk’):

  • 在主app module中有一个 @Modules({“app”, “sdk”}) 注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类
  • 在 RouterInit类 的 init方法 中生成调用同一个包内的 RouterMapping_app.map
  • 每个 module 生成的类(RouterMapping_app.java 和 RouterMapping_sdk.java)都放在com.github.mzule.activityrouter.router包内(在不同的aar中,但包名相同)
  • 在 RouterMapping_sdk类 的 map()方法 中根据扫描到的当前 module 内所有路由注解,生成了调用Routers.map(…)方法来注册路由的代码
  • 在 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中配置一下扫描的参数,没有任何代码侵入,原理详细介绍:

http://blog.csdn.net/cdecde111/article/details/78074692

如何兼容同步/异步方式调用组件?

通过实现 java.util.concurrent.Callable 接口同步返回结果来兼容同步/异步调用:

  • 同步调用时,直接调用 CCResult result = Callable.call() 来获取返回结果
  • 异步调用时,将其放入线程池中运行,执行完成后调用回调对象返回结果: IComponentCallback.onResult(cc, result)

ExecutorService.submit(callable)

如何兼容同步/异步方式实现组件?

调用组件的 onCall方法 时,可能需要异步实现,并不能同步返回结果,但同步调用时又需要返回结果,这是一对矛盾。

此处用到了 Object 的wait-notify机制,当组件需要异步返回结果时,在CC框架内部进行阻塞,等到结果返回时,通过notify中止阻塞,返回结果给调用方

注意,这里要求在实现一个组件时,必须确保组件一定会回调结果,即:需要确保每一种导致调用流程结束的逻辑分支上(包括if-else/try-catch/Activity.finish()-back键-返回按钮等等)都会回调结果,否则会导致调用方一直阻塞等待结果,直至超时。类似于向服务器发送一个网络请求后服务器必须返回请求结果一样,否则会导致请求超时。

如何进行跨进程组件任意功能的调用(不只是启动Activity)?

市面上常见的组件化框架采用的通信解决方案有:

URLScheme(例如:ActivityRouter、ARouter等)

  • 优势有:基因中自带支持从webview中调用不用互相注册(不用知道需要调用的app的进程名称等信息)
  • 劣势有:只能单向地给组件发送信息,适用于启动Activity和发送指令,不适用于获取数据(例如:获取用户组件的当前用户登录信息)需要有个额外的中转Activity来统一处理URLScheme
    如果设备上安装了多个使用相同URLScheme的app,会弹出选择框(多个组件作为app同时安装到设备上时会出现这个问题)
    无法进行权限设置,无法进行开关设置,存在安全性风险

AIDL (例如:ModularizationArchitecture)

  • 优势有:可以传递Parcelable类型的对象效率高
    可以设置跨app调用的开关
  • 劣势有:调用组件之前需要提前知道该组件在那个进程,否则无法建立ServiceConnection组件在作为独立app和作为lib打包到主app时,进程名称不同,维护成本高

设计此功能时,我的出发点是:作为组件化开发框架基础库,想尽量让跨进程调用与在进程内部调用的功能一致,对使用此框架的开发者在切换app模式和lib模式时尽量简单,另外需要尽量不影响产品安全性。因此,跨组件间通信实现的同时,应该满足以下条件:

  • 每个app都能给其它app调用
  • app可以设置是否对外提供跨进程组件调用的支持
  • 组件调用的请求发出去之后,能自动探测当前设备上是否有支持此次调用的app
  • 支持超时、取消

基于这些需求,我最终选择了 BroadcastReceiver + Service + LocalSocket 来作为最终解决方案:

如果 appA 内发起了一个当前app内不存在的组件:Component1,则建立一个LocalServerSocket,同时发送广播给设备上安装的其它同样使用了此框架的 app,同时,若某个 appB 内支持此组件,则根据广播中带来的信息与 LocalServerSocket 建立连接,并在 appB 内调用组件 Component1,并将结果通过 LocalSocket 发送给 appA。

BroadcastReceiver 是 android 四大组件之一,可以设置接收权限,能避免外部恶意调用。并且可以设置开关,接收到此广播后决定是否响应(假装没接收到…)。

之所以建立 LocalSocket 链接,是为了能继续给这次组件调用请求发送超时和取消的指令。

用这种方式实现时,遇到了3个问题:

  • 由于广播接收器定义在基础库中,所有app内都有,当用户在主线程中同步调用跨app的组件时,调用方主线程被阻塞,广播接收器也在需要主线程中运行,导致广播接收器无法运行,直至timeout,组件调用失败。将广播接收器放到子进程中运行问题得到解决
  • 被调用的app未启动或被手动结束进程,遇到广播接收不到的问题这个问题暂时未很好的解决,但考虑到组件化开发只在开发期间需要用到跨进程通信,开发者可以通过手动在系统设置中给对应的app赋予自启动权限来解决问题
  • 跨进程调用时,只能传递基本数据类型,无法获取Fragment等java对象这个问题在app内部调用时不存在,app内部来回传递的都是Map,可以传递任何数据类型。但由于进程间通信是通过字符串来回发送的,暂时支持不了非基本数据类型,未来可以考虑支持Serializable

组件如何更方便地在application和library之间切换?

关于切换方式在网络上有很多文章介绍,基本上都是一个思路:在 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'

如何实现startActivityForResult?

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个安全级别可以设置:

  • 权限验证(给进程间通信的广播设置权限,一般可设置为签名级权限校验),步骤如下:
  • 新建一个module
  • 在该module的build.gradle中添加对基础库的依赖,如: compile ‘com.billy.android:cc:0.3.0′
  • 在该module的src/main/AndroidManifest.xml中设置权限及权限的级别,参考component_protect_demo
  • 其它每个module都额外依赖此module,或自定义一个全局的cc-settings.gradle,参考cc-settings-demo-b.gradle
  • 外部调用是否响应的开关设置(这种方式使用起来更简单一些)
  • 在Application.onCreate()中调用CC.enableRemoteCC(false)可关闭响应外部调用

为了方便开发者接入,默认是开启了对外部组件调用的支持,并且不需要权限验证。app正式发布前,建议调用 CC.enableRemoteCC(false) 来关闭响应外部调用本app的组件。

如何与Activity、Fragment的生命周期关联起来

背景:在使用异步调用时,由于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调用

  • andorid.app.Fragment生命周期关联(暂不支持)

CC执行流程详细解析

组件间通信采用了组件总线的方式,在基础库的组件管理类(ComponentMananger)中注册了所有组件对象,ComponentMananger通过查找映射表找到组件对象并调用。

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

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

没有:执行App之间CC调用的流程

组件的同步/异步实现和组件的同步/异步调用原理

组件实现时,当组件调用的相关功能结束后,通过CC.sendCCResult(callId, ccResult)将调用结果发送给框架

IComponent实现类(组件入口类)onCall(cc)方法的返回值代表是否异步回调结果:

  • true: 将异步调用CC.sendCCResult(callId, ccResult)
  • false: 将同步调用CC.sendCCResult(callId, ccResult)。意味着在onCal方法执行完之前会调用此方法将结果发给框架

当IComponent.onCall(cc)返回 false 时,直接获取CCResult并返回给调用方

当IComponent.onCall(cc)返回true时,将进入wait()阻塞,知道获得CCResult后通过notify()中止阻塞,继续运行,将CCResult返回给调用方

通过ComponentManager调用组件时,创建一个实现了java.util.concurrent.Callable接口ChainProcessor类来负责具体组件的调用

  • 同步调用时,直接执行ChainProcessor.call()来调用组件,并将CCResult直接返回给调用方
  • 异步调用时,将ChainProcessor放入线程池中执行,通过IComponentCallback.onResult(cc, ccResult)将CCResult回调给调用方

执行过程如下图所示:

自定义拦截器(ICCInterceptor)实现原理

所有拦截器按顺序存放在调用链(Chain)中

在自定义拦截器之前有1个CC框架自身的拦截器:

  • ValidateInterceptor

在自定义拦截器之后有2个CC框架自身的拦截器:

  • LocalCCInterceptor(或RemoteCCInterceptor)
  • Wait4ResultInterceptor

Chain类负责依次执行所有拦截器interceptor.intercept(chain)

拦截器intercept(chain)方法通过调用Chain.proceed()方法获取CCResult

App内部CC调用流程

当要调用的组件在当前app内部时,执行此流程,完整流程图如下:

CC的主体功能由一个个拦截器(ICCInterceptor)来完成,拦截器形成一个调用链(Chain),调用链由ChainProcessor启动执行,ChainProcessor对象在ComponentManager中被创建。

因此,可以将ChainProcessor看做一个整体,由ComponentManager创建后,调用组件的onCall方法,并将组件执行后的结果返回给调用方。

ChainProcessor内部的Wait4ResultInterceptor

ChainProcessor的执行过程可以被timeout和cancel两种事件中止。

App之间CC调用流程

当要调用的组件在当前app内找不到时,执行此流程,完整流程图如下:

结语

本文比较详细地介绍了android组件化开发框架《CC》的主要功能、技术方案及执行流程,并给出了使用方式的简单示例。

大家如果感兴趣的话可以从GitHub上clone源码来进行具体的分析,如果有更好的思路和方案也欢迎贡献代码进一步完善CC。

转载请注明:Android开发中文站 » 教你打造一个Android组件化开发框架

单独组件_阿里P8年薪百万大牛-教你打造一个Android组件化开发框架相关推荐

  1. android组件化开发视频教程,教你打造一个Android组件化开发框架

    作者简介 本篇来自 lucky_billy 的投稿,分享了他的开源组件化框架,详细地讲解框架形成的思路,希望对大家有所帮助. lucky_billy 的博客地址: 解读开源框架设计思想 B站学习视频 ...

  2. 教你打造一个Android组件化开发框架

    *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 CC:Component Caller,一个android组件化开发框架, 已开源,github地址:https://github ...

  3. 草图大师里创建动态组件_【干货】SketchUp建模|教你做一个有生命的家具动态组件...

    作者|活力网-Andrew 同学们大扎好! 本次专题来自SU建模情报局 其实Andrew准备了10几个建模案例 但是都没有时间录制成视频 之后也会加快更新的速度 本期是我们的 本期案例介绍 为家具模型 ...

  4. 阿里java架构师+职级_阿里 P8 高级架构师年入100W+,到底什么水平?

    原标题:阿里 P8 高级架构师年入100W+,到底什么水平? 大家都知道,阿里P8高级技术专家,基本上是一线技术人能达到的最高职级,也是很多程序员追求的目标.达到年入百万的P8 Java高级架构师级别 ...

  5. springcloud是什么_阿里P8道出,入职阿里必会199道SpringCloud面试题,你能掌握多少?...

    前言 Spring Cloud 自 2016 年 1 月发布第一个 Angel.SR5 版本,到目前 2020 年 3 月发布 Hoxton.SR3 版本,已经历经了 4 年时间.这 4 年时间里,S ...

  6. psql where里有自定义函数慢_阿里P8架构师谈:MySQL慢查询优化、索引优化、以及表等优化总结...

    MySQL优化概述 MySQL数据库常见的两个瓶颈是:CPU和I/O的瓶颈. CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候. 磁盘I/O瓶颈发生在装入数据远大于内存容量的时候,如果应 ...

  7. 阿里对mysql的优化_阿里P8架构师谈:MySQL慢查询优化、索引优化、以及表等优化总结...

    更多内容:https://www.toutiao.com/i6599796228886626829/?tt_from=weixin&utm_campaign=client_share& ...

  8. python android开发月薪_同事逆袭面进阿里P7 年薪60W+,临别留下一张Android开发重点技术路线图…...

    现在 通过职友集 北京 Android 工程师「 而今年因为 如果你在中小型公司,其实 20K 左右,几乎已经是个极限,薪资想继续提升很困难.而在 首先我们先看一下岗位职责: 1.负责 岗位要求: 1 ...

  9. 蚂蚁p8多少股票_阿里P8收入有多高?满足大家的好奇,司龄不足10年,税前330万...

    小时候,萦绕耳边的一句话叫"别人家的孩子":长大后,就变成了:"别人家的工资".薪资作为企业的软实力的代表,直接影响了职工的工作幸福度.总有人一言不合就开始秀薪 ...

最新文章

  1. 第一讲SQL命令的DDL和DML操作讲解
  2. SAP Spartacus header区域布局设计
  3. ubuntu安装chrome driver
  4. java如何使用promql_Prometheus 常用 PromQL 语句
  5. Ubuntu 为普通用户添加sudo权限
  6. Wireshark系列之7 利用WinHex还原文件
  7. PhotoShop2018安装与破解教程
  8. 破解qq上网限制 突破限制上QQ
  9. android自定义4级联动,使用Android如何实现四级联动地址选择器
  10. 实验一、安卓开发环境基本配置实验
  11. 百度收录如何API提交(java、python)
  12. 【无标题】setting.json 配置
  13. 通过链接下载google drive文件
  14. 以防遗忘001_通过斜率求垂直线段的端点,附Unity(UI image)画线
  15. The server cannot or will not process the request due to something that is perceived to be a client.
  16. 树莓派 cpolar免费vip穿透局域网异地连接(ssh web均可)
  17. 香蕉派 BPI-P2 Zero 四核开源物联网开发板,支持PoE网络供电
  18. 《机械工程基础》复习题
  19. 真值 原码 补码 反码 移码
  20. 基于云的先进计划与调度系统的框架与部署

热门文章

  1. java 保存文件在服务器_java文件保存至服务器
  2. 旋流式沉砂池计算_旋流沉砂池设计方法
  3. octave错误-error: ‘squareThisNumber‘ undefined near line 1 column 1
  4. python之集合与字典
  5. 【文章】一副对联,便写尽了人生
  6. 以VMware workstation15.5.2pro为例如何新建虚拟机,安装镜像文件(最新)
  7. 进程控制(二)与linux下的自有服务
  8. 发一则自己创作的Lae程序员小漫画,仅供一乐
  9. 利用Unity实现AOP
  10. 关闭应用程序的几种方法