面试的点各不相同,有组件化架构设计、插件化框架设计、路由架构设计、热修复设计等问题,但是最终都是殊途同归,所有的问题都汇集在这,如何对手机淘宝组架构设计?
组件化如何实现,组件化与插件化的差别在 哪里,该怎么选型
**面试官:**组件化如何实现,组件化与插件化的差别在哪里,该怎么
选型
**心理分析:**面试官从架构层次 了解求职者是否用过 模块化 组件 化 和插件化,在过去经验有没有运用过这些技术到项目中,这道 题属于一个连环炮。求职者该格外小心**求职者:**应该从 App 开发的需求来定义技术选型,分别说说模 块化,组件化 插件化的优势和区别
一、组件化
组件化,就是把 APP 拆分成不同功能模块,形成独立组件,让宿主调用。 组件 化不一定是插件化,组件化是一个更大的概念:把模块解耦,组件之间代码不依 赖,宿主可以依赖组件;而插件化则具体到了技术点上,宿主通过 动态加载 来 调用组件,宿主不依赖组件,达到 完全解耦 的目的(比如图片缓存就可以看成 一个组件被多个 App 共用)。
**适合于项目大 但是功能相对集中。**比如 一个金融类的 App 里面只包含金融的 功能,金融功能又会有 借贷,理财,线下交易,把这些模块抽成单独的组件 二、插件化 Android 程序每次更新都要下载一个完整的 apk,而很多时候软件只是更新了一 个小功能而已,这样的话,就显得很麻烦。如果把 android 程序做成主程序+插 件化的形式呢,这样才利于小功能的扩展(比如一般 App 的皮肤样式就可以看 成一个插件)。
二、插件化
Android 程序每次更新都要下载一个完整的 apk,而很多时候软件只是更新了一 个小功能而已,这样的话,就显得很麻烦。如果把 android 程序做成主程序+插 件化的形式呢,这样才利于小功能的扩展(比如一般 App 的皮肤样式就可以看 成一个插件)。
通过 gradle 配置的方式,将打 debug 包和 release 包分开。这 样会有一个好处,开发一个模块,在 debug 的时候,可以打成一 个 apk ,独立运行测试,可以完全独立于整个宿主 APP 的其他 所有组件;待到要打 release 包的时候,再把这个模块作为一个 library ,打成 aar ,作为整个宿主 APP 的一部分。而 debug 和 release 的切换都是通过 gradle 配置,可以做到无缝切换。至于 模块之间的跳转,可以用别名的方式,而不是用 Activity 和 Fragment 类名。这样所有的模块和宿主 APP 都是完全解耦的, 彻底解决了并行开发的可能造成的交叉依赖等问题
主要原理是:主要利用 Java ClassLoader 的原理,如 Android 的 DexClassLoader,可动态加载的内容包括 apk、dex、jar 等。如下
插件化的优势:
- 适应并行开发,解耦各个模块,避免模块之间的交叉依赖,加快编译速度, 从而提高并行开发效率。
- 满足产品随时上线的需求
- 修复因为我们对自己要求不严格而写出来的 bug。
- 插件化的结果:分为稳定的 release 版本和不稳定的 snapshot 版本,每 个模块都高度解耦,没有交叉依赖,不会出现一个模块依赖了另一个模块, 其中一个人改了这个模块的代码,对另一个模块造成影响。
淘宝的框架是用了 osgi 的 bundle 概念,整个应用框架生命周期完整。 **适合于项目超级大 但是功能相对不集中。**比如 一个支付宝 App 里面即包 含共享单车 也包含 电影票。这种与本业务完全不同的 可以做成插件的形式 插件化弊端: 每一个插件都是一个 apk,插件多的时候管理起来也麻烦。
说下组件之间的跳转和组件通信原理机制
面试官: 说下组件之间的跳转和组件通信原理机制
**心理分析:**面试官从架构层次 了解求职者是否对组件化有深入研 究。是否使用过组件化,使用有多深。通过该问题一目了然。如果 能说出项目的演进 组件通信选型 绝对是一个加分项
**求职者:**应该从为什么会用到组件化 和组件定义,组件通信的 演进说起
我们公司的一个单体项目进行组件化架构改造,我们最开始从以下 7 个方面入手:
- 代码解耦。如何将一个庞大的工程分成有机的整体?
- 组件单独运行。因为每个组件都是高度内聚的,是一个完整的整体,如何 让其单独运行和调试?
- 组件间通信。由于每个组件具体实现细节都互相不了解,但每个组件都需 要给其他调用方提供服务,那么主项目与组件、组件与组件之间如何通信 就变成关键?
- UI 跳转。UI 跳转指的是特殊的数据传递,跟组件间通信区别有什么不 同?
- 组件生命周期。这里的生命周期指的是组件在应用中存在的时间,组件是 否可以做到按需、动态使用、因此就会涉及到组件加载、卸载等管理问题。
- 集成调试。在开发阶段如何做到按需编译组件?一次调试中可能有一两个 组件参与集成,这样编译时间就会大大降低,提高开发效率。
- 代码隔离。组件之间的交互如果还是直接引用的话,那么组件之间根本没 有做到解耦,如何从根本上避免组件之间的直接引用,也就是如何从根本 上杜绝耦合的产生?
今天则会从更小细粒度入手,主要讲讲在组件化架构下组件与组件之间通信机制 是如何、包括所谓的 UI 跳转,其实也是组件化通信,只不过它稍微特殊点,单 独抽取出来而已。学习知识的过程很常见的一个思路就是从整体概况入手,首先 对整体有个粗略的印象,然后再深入细节,抽丝剥茧般去挖掘其中的内在原理, 一个点一个不断去突破,这样就能建立起自己整个知识树,所以今天我们就从通 信机制这个点入手,看看其中内在玄机有哪些。
思维导图
同样,在每写一篇文章之前,放个思维导图,这样做的好处对于想写的内容有很 好的梳理,逻辑和结构上显得清晰点。
总所周知,Android 提供了很多不同的信息的传递方式,比如在四大组件中本地 广播、进程间的 AIDL、匿名间的内存共享、Intent Bundle 传递等等,那么在这 么多传递方式,哪种类型是比较适合组件与组件直接的传递呢。
- 本地广播,也就是 LoacalBroadcastRecevier。更多是用在同一个应用内的不同系 统规定的组件进行通信,好处在于:发送的广播只会在自己的 APP 内传播,不 会泄漏给其他的 APP,其他 APP 无法向自己的 APP 发送广播,不用被其他 APP 干扰。本地广播好比对讲通信,成本低,效率高,但有个缺点就是两者通信机制 全部委托与系统负责,我们无法干预传输途中的任何步骤,不可控制,一般在组 件化通信过程中采用比例不高。
- 进程间的 AIDL。这个粒度在于进程,而我们组件化通信过程往往是在线程中, 况且 AIDL 通信也是属于系统级通信,底层以 Binder 机制,虽说 Android 提供模 板供我们实现,但往往使用者不好理解,交互比较复杂,往往也不适用应用于组 件化通信过程中。
- 匿名的内存共享。比如用 Sharedpreferences,在处于多线程场景下,往往会线 程不安全,这种更多是存储一一些变化很少的信息,比如说组件里的配置信息等 等。
- Intent Bundle 传递。包括显性和隐性传递,显性传递需要明确包名路径,组件 与组件往往是需要互相依赖,这背离组件化中 SOP(关注点分离原则),如果走 隐性的话,不仅包名路径不能重复,需要定义一套规则,只有一个包名路径出错, 排查起来也稍显麻烦,这个方式往往在组件间内部传递会比较合适,组件外与其 他组件打交道则使用场景不多。
说了这么多,那组件化通信什么机制比较适合呢?既然组件层中的模块是相互独 立的,它们之间并不存在任何依赖。没有依赖就无法产生关系,没有关系,就无 法传递消息,那要如何才能完成这种交流?
目前主流做法之一就是引入第三者,比如图中的 Base Module。
基础组件化架构
组件层的模块都依赖于基础层,从而产生第三者联系,这种第三者联系最终会编 译在 APP Module 中,那时将不会有这种隔阂,那么其中的 Base Module 就是 跨越组件化层级的关键,也是模块间信息交流的基础。比较有代表性的组件化开 源框架有得到 DDComponentForAndroid、Arouter、聚美 Router 等等。
除了这种以通过引入第三者方式,还有一种解决方式是以事件总线方式,但这种 方式目前开源的框架中使用比例不高,如图:
事件总线
事件总线通过记录对象,使用监听者模式来通知对象各种事件,比如在现实生活 中,我们要去找房子,一般都去看小区的公告栏,因为那边会经常发布一些出租 信息,我们去查看的过程中就形成了订阅的关系,只不过这种是被动去订阅,因 为只有自己需要找房子了才去看,平时一般不会去看。小区中的公告栏可以想象 成一个事件总线发布点,监听者则是哪些想要找房子的人,当有房东在公告栏上 贴上出租房信息时,如果公告栏有订阅信息功能,比如引入门卫保安,已经把之 前来这个公告栏要查看的找房子人一一进行电话登记,那么一旦有新出租消息产 生,则门卫会把这条消息一一进行短信群发,那么找房子人则会收到这条消息进 行后续的操作,是马上过来看,还是延迟过来,则根据自己的实际情况进行处理。 在目前开源库中,有 EventBus、RxBus 就是采用这种发布/订阅模式,优点是简 化了 Android 组件之间的通信方式,实现解耦,让业务代码更加简洁,可以动态 设置事件处理线程和优先级,缺点则是每个事件需要维护一个事件类,造成事件 类太多,无形中加大了维护成本。那么在组件化开源框架中有 ModuleBus、CC 等 等。
这两者模式更详细的对比,可以查看这篇文章多个维度对比一些有代表性的开源 android 组件化开发方案
实现方案
事件总线,又可以叫做组件总线,路由+接口,则相对好理解点,今天从阅读它 们框架源码,我们来对比这两种实现方案的不同之处。
组件总线
这边选取的是 ModuleBus 框架,这个方案特别之处在于其借鉴了 EventBus 的思 想,组件的注册/注销和组件调用的事件发送都跟 EventBus 类似,能够传递一些 基础类型的数据,而并不需要在 Base Moudel 中添加额外的类。所以不会影响 Base 模块的架构,但是无法动态移除信息接收端的代码,而自定义的事件信息 类型还是需要添加到 Base Module 中才能让其他功能模块索引。
其中的核心代码是在与 ModuleBus 类,其内部维护了两个 ArrayMap 键对值列 表,如下:
private static ArrayMap<Object,ArrayMap<String,MethodInfo>>
moduleEventMethods = new ArrayMap<>();
private static ArrayMap<Class<?>,ArrayMap<String,ArrayList<Object>>>
moduleMethodClient = new ArrayMap<>()
在使用方法上,在 onCreate()和 onDestroy()中需要注册和解绑,比如
ModuleBus.getInstance().register(this);
ModuleBus.getInstance().unregister(this);
最终使用类似 EventBus 中 post 方法一样,进行两个组件间的通信。这个框架 的封装的 post 方法如下
public void post(Class<?> clientClass,String methodName,Object...args){
if(clientClass == null || methodName == null ||methodName.length() == 0) return;
ArrayList<Object> clientList = getClient(clientClass,methodName)
for(Object c: clientList){ ArrayMap<String,MethodInfo> methods = moduleEventMethods.get(c);
Method method = methods.get(methodName).m;
method.invoke(c,args);
}
可以看到,它是通过遍历之前内部的 ArrayMap,把注册在里面的方法找出,根据传入的参数进行匹配,使用反射调用。
接口+路由
接口+路由实现方式则相对容易理解点,我之前实践的一个项目就是通过这种方 式实现的。具体地址如下:DemoComponent 实现思路是专门抽取一个 LibModule 作为路由服务,每个组件声明自己提供的服务 Service API,这些 Service 都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router 中去,如果要使用某个组件的功能,只需要向 Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。 比如定义两个路由地址,一个登陆组件,一个设置组件,核心代码:
public class RouterPath {
public static final String ROUTER_PATH_TO_LOGIN_SERVICE = "/login/service";
public static final String ROUTER_PATH_TO_SETTING_SERVICE = "/setting/service"; }
那么就相应着就有两个接口 API,如下:
public interface ILoginProvider extends IProvider {
void goToLogin(Activity activity);
}
public interface ISettingProvider extends IProvider {
void goToSetting(Activity activity);
}
}
这两个接口 API 对应着是向外暴露这两个组件的能提供的通信能力,然后每个组 件对接口进行实现,如下:
@Override
public void init(Context context) {
}
@Override
public void goToLogin(Activity activity) {
Intent loginIntent = new Intent(activity, LoginActivity.class);
activity.startActivity(loginIntent);
}
}
这其中使用的到了 ARouter 页面跳转方式,内部本质也是接口+实现方式 进行组件间通信。
调用则很简单了,如下:
ILoginProvider loginService = (ILoginProvider)
ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).naviga tion();
if(loginService != null){
loginService.goToLogin(MainActivity.this);
}
还有一个组件化框架,就是 ModularizationArchitecture ,它本质实现方式也是 接口+实现,但是封装形式稍微不一样点,它是每个功能模块中需要使用注解建 立 Action 事件,每个 Action 完成一个事件动作。invoke 只是方法名为反射,并 未用到反射,而是使用接口方式调用,参数是通过 HashMap 传递的,无法传递 对象。具体详解可以看这篇文章 Android 架构思考(模块化、多进程)。,string>
页面跳转
页面跳转也算是一种组件间的通信,只不过它相对粒度更细化点,之前我们描述 的组件间通信粒度会更抽象点,页面跳转则是定位到某个组件的某个页面,可能 是某个 Activity,或者某个 Fragment,要跳转到另外一个组件的 Activity 或 Fragment,是这两者之间的通信。甚至在一般没有进行组件化架构的工程项目 中,往往也会封装页面之间的跳转代码类,往往也会有路由中心的概念。不过一 般 UI 跳转基本都会单独处理,一般通过短链的方式来跳转到具体的 Activity。 每个组件可以注册自己所能处理的短链的 Scheme 和 Host,并定义传输数据的 格式,然后注册到统一的 UIRouter 中,UIRouter 通过 Scheme 和 Host 的匹 配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity 上添加注 解,然后通过 APT 形成具体的逻辑代码。
下面简单介绍目前比较主流的两个框架核心实现思路:
ARouter
ARouter 核心实现思路是,我们在代码里加入的@Route 注解,会在编译时期通 过 apt 生成一些存储 path 和 activityClass 映射关系的类文件,然后 app 进程启 动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在 map 里),然后在进行路由跳转的时候,通过 build()方法传入要到达页面的路由 地址,ARouter 会通过它自己存储的路由表找到路由地址对应的 Activity.class(activity.class = map.get(path)),然后 new Intent(),当调用 ARouter 的 withString()方法它的内部会调用 intent.putExtra(String name, String value), 调用 navigation()方法,它的内部会调用 startActivity(intent)进行跳转,这样便可 以实现两个相互没有依赖的 module 顺利的启动对方的 Activity 了。
ActivityRouter ActivityRouter
核心实现思路是,它是通过路由 + 静态方法来实现,在静态方 法上加注解来暴露服务,但不支持返回值,且参数固定位(context, bundle),基 于 apt 技术,通过注解方式来实现 URL 打开 Activity 功能,并支持在 WebView 和外部浏览器使用,支持多级 Activity 跳转,支持 Bundle、Uri 参数注入并转换 参数类型。它实现相对简单点,也是比较早期比较流行的做法,不过学习它也是 很有参考意义的。
小结
**总的来说,**组件间的通信机制在组件化编程和组件化架构中是很重要的一个环 节,可能在每个组件独自开发阶段,不需要与其他组件进行通信,只需要在内部 通信即可,当处于组件集成阶段,那就需要大量组件进行互相通信,体现在每个 业务互相协作,如果组件间设计的不好,打开一个页面或调用一个方法,想当耗 时或响应慢,那么体现的则是这个 APP 使用比较卡顿,仅仅打开一个页面就是 需要好几秒才能打开,则严重影响使用者的体验了,甚至一些大型 APP,可能组 件分化更小,种类更多,那么组件间的通信则至关重要了。所以,要打造一个良好的组件化框架,如何设计一个更适合自己本身的业务类型的通信机制,就需要 多多进行思考了。
更多关于组件化问题,我都整理为文档形式了,需要的小伙伴可以简信我【666】或者点击码云地址获取:https://gitee.com/androidmaniu/android-notes/blob/master/README.md