本文首发于51NB技术公众号,原文连接 51信用卡Android架构演进实践前端
随着业务的快速扩张,本来小做坊式的单个工程的开发模式越来与不能知足实际需求。早在两年多之前,51信用卡管家就向下沉淀出了单独的公用基础库,一些通用的功能组件和个别独立的业务被拆分红 SDK,造成了一套中型项目、多人并行的开发模式,也为将来组件化拆分作准备。java
这套框架运行了一段时间以后,伴随着单应用内业务需求的增长、开发人员数量的增多、基础库数量的膨胀,致使了一些问题:android
除了以上问题,动态化需求也愈来愈强烈,依赖 Hybrid + H5 打开页面慢的问题也凸显出来。git
这些问题推进咱们更进一步的升级开发构架。github
最近两年,插件化框架层出不穷,各大厂都放出了自家开源的插件化框架。做为 Native 动态化与性能兼顾的插件化方案,不少公司选择插件化做为动态化技术方案。动态性一般有两部分的做用:一是动态热修复;二是动态下发业务插件。对于第一点,咱们有热修复框架能够完成这部分工做;对于第二点,咱们使用了 Hybrid 加载H5的方式实现,虽然性能上有所欠缺,但彻底切到 Native 来作有点推倒重来的意思,而且跟业界同窗交流后,对于动态下发业务插件用到的状况也很少,业务更新主要仍是依靠 App 升级来实现。技术方案没有最优解,选择适合本身的才是最好的。web
因为插件化也存在一些弊端,好比不可避免的 hook framework、修改 aapt、包装 Gradle Plugin、代理组件等等很是规操做,平常维护也是一笔不小的开销,稳定性、兼容性、新版本适配等等问题都须要考虑进去。对于 Android 端是否使用插件化,公司内部作过一些讨论,结论是不急着上,边走边看,先把业务组件拆分出来再说。sql
现在回过头看,自从 Android P发布以来,限制 hook framework 后,插件化逐渐开始式微,后面走向大几率是维护成本愈来愈高,成本收益比逐渐下降,最终弃坑不用。数据库
除了插件化外,动态化方案近两年比较火的就是以 ReactNative、Weex 为表明的大前端方向,结合51信用卡的实际状况,最终选择拥抱大前端, Weex 做为动态化方案,以 Native 为主, Hybrid 离线化方案为辅,Weex 逐步迭代的架构开发模式。编程
Weex 的基础建设和前端同窗合做,历经大半年时间,目前已经稳定应用在51信用卡各个 App 上,Weex 做为动态化页面的首选方案,已经完成了线上数百个页面的开发需求。配合离线化方案,各项性能指标也都达到要求。api
代码解耦与代码隔离,最有效的方案是工程隔离。审视咱们最初的方案,每一个 SDK 对应单独的仓库,经过 maven 依赖,经过工程分离隔离代码,这种方案没有问题,只不过须要往前更近一步,各个业务模块也须要独立主工程,拆分红独立的业务组件。
同时,划分清楚代码边界,控制依赖关系,梳理清楚层次结构,最终造成以下图所示的架构。
总体架构上提供三种容器:
同时,Hybrid 和 Weex 依赖于原生提供的方法,经过 JsBridge 进行通讯,目前共有 200 多个 PG 方法供 js 调用。长远来看,这三种容器并不会互相取代,相反地,它们应该是相互依存、取长补短、长期共存的状态。
Native 容器对应上图中各个层级的定义:
组件化拆分的核心诉求是解耦合,提升组件内聚,因此应该从诉求出发,在沿用当下开发模式,而且不强依赖组件化框架的状况下,逐渐的进行组件化拆分。
经过工程隔离进而进行组件化拆分后,基本能够解决上面提到的问题:
解耦,通常须要避免直接依赖,转为间接依赖,简单来讲就是依赖隔离。对于组件化而言,每一个组件都是单独的实现,单个组件对外提供的服务尽量单一,依赖尽量少;同时,依赖其它组件功能或页面的状况下,尽量避免直接依赖,最好依赖中间层进行集中式管理,而后再进行逻辑分发。因此咱们通常采用分总分的结构:组件内部分别注册,编译时生成汇总代码、运行时集中式管理,调用时处理逻辑分发。
组件化须要解耦处理的几个基础模块:
下面依次介绍。
路由分发本质上是把直接依赖引用转化为中心化管理分发的一个过程,因为组件化拆分后,各个业务组件间不存在直接的依赖关系,因此必然要有一个统一收集页面跳转规则进而再分发的过程。
51信用卡在 2017 年就在进行路由化实践,以应对后面进行的组件化拆分需求,并沉淀出一套自研的路由框架 U51OkDeepLink,它也采用分总分结构,主要原理是组件内注册路由,编译时在组件内生成独立的路由表,并用 AOP 在编译时作好全部组件内路由表汇总的工做,调用初始化方法时进行路由表汇总,页面跳转时再进行管理分发,其用法很简单:
//组件内注册路由
public interface SampleService {
@Path("/main")
@Activity(MainActivity.class)
void startMainActivity(@Query("key") String key);
}
//其他组件唤起页面
new DeepLinkClient(context).buildRequest("old://app/main?key=value").addQuery("key2", "2").start();
复制代码
而且支持强大的异步特性,支持跳转过程当中的中间逻辑处理。
其原理图以下
感兴趣的读者能够阅读 Android 组件化 —— 路由设计最佳实践 获取更多技术细节。
组件间层次和边界模糊问题的产生,根本缘由是各个业务组件间的相互依赖关系混乱,为了进行业务组件间的隔离,首先要作好组件之间的服务调用解耦。
这里采用的是 ServiceLoader 的模式,组件工程目录通常以下所示
每一个组件内通常声明三个 module:
业务组件之间依赖 api 库的服务接口,imp 库做为实现动态查找。版本发布时,同时发布 api 和 imp 两个库,而且保证 api 和 imp 具备相同版本号,这个在组件发版时统一管理。
//组件内 api module 接口声明
@Service
public interface TestService {
void sayHello();
}
//组件内 imp module 接口实现
@ServiceImpl
public class TestServiceImpl implements TestService {
@Override
public void sayHello() {
}
}
//跨组件调用
compile 'com.u51.android:test-lib-api:$version'
CommentService service = ServicesLoader.getInstance().getService(TestService.class);
service.sayHello();
复制代码
它的实现原理与路由相似,也是采用分总分结构,在编译时经过 APT 生成汇总代码,调用时动态查找注入 Service 及其实现类的绑定关系。
与路由初始化汇总路由表不一样的是,ServiceLoader 在调用时查找,省去了初始化的逻辑,Service 不会像路由这么多,查找起来不会存在遍历太慢的问题。
消息总线是基于 EventBus 实现的跨三端(Native、Hybrid、Weex)事件管理分发组件 U51EventBus。跨三端是指在任意一端注册监听后,在事件触发时均可以获得响应。
对于原生开发来讲,EventBus 自己能够知足需求,虽然有点事件满天飞的缺点,可是还在可接受范围以内。对于业务组件来讲,其 Event 类须要放在 api module 中进行暴露。
对于 Hybrid 和 Weex 来讲,通常的 bridge 都是 callback 形式获得异步响应,对于全局事件通知支持不太友好。经过 bridge 通道链接 U51EventBus 消息总线,打通跨三端全局的事件监听及分发,得以实现任意事件能够在 Native、H五、Weex 之间相互发送和监听。好比,相似登录、登出操做在 Native 发出后,全局已打开的 H5 或 Weex 页面能够当即获得感知。
其实现原理也是采用分总分结构,在编译时对 EventBus 进行了定制封装,事件分发仍是使用的原有的 EventBus 分发逻辑。
数据存储采用基于 Room 实现的统一 KV 存储框架,底层数据库依然是 sqlite,性能这块没有作特别强调,强制其在子线程中进行操做,用于支持平常开发中配置和业务数据的存取操做。
另外,数据总线支持按模块进行存取,每一个业务组件均可以定义自有 tag,避免字段冲突问题。
不管从早期的 PhoneGap、Cordova,仍是近年来比较火的 ReactNative、Weex,到最近两年崛起的 Flutter,跨平台混合开发一直深受众多开发青睐。究其缘由,仍是其跨平台和动态化是原生开发所不具有的特性。
Native 和 H5 混合开发通常是比较常见的混合开发模式,H5 开发效率高、迭代快速、不依赖 App 发版,51信用卡众多 App 产品中,有不少页面都是用 H5 来开发,嵌入原生 App 中使用 webview 进行加载显示。
早期 H5 容器在各个 App 中分别独立实现,没有统一的架构和规范,致使对 H5 的支持效率较低,PG 方法(来源于 PhoneGap)的开发、测试和维护都至关的混乱,重复性工做太多。
Native 层提供一套通用性强、功能丰富、稳定性高的 H5 容器对业务的高速发展相当重要。
因为 H5 不具有直接调用原生方法,因此原生壳要提供一套通用的通讯方式,通常为 JsBridge,在 Android 端,实现 JsBridge 通讯的通道通常有如下几种:
而通道不是关键,怎样管理和维护 PG 方法调用才是核心。为此,咱们把每一个方法定义为一个 Plugin,用插件的形式管理 PG 方法,这样能够作到每一个插件独立运行,互不干扰。插件管理也是采用分总分结构,在各个业务组件中分别注册,编译是经过 APT 生成汇总代码,运行时进行插件汇总,最后调用经过 PluginManager 查找分发逻辑。
插件注册代码以下,其中 onExecute()
方法在 js 调用该方法时触发,执行结果经过 evaluateJavaScript()
方法异步返回。
@JsPlugin(name = TestPlugin.PLUGIN_NAME, loadOnInit = false, version = 1)
public class TestPlugin extends EnNiuJsPlugin {
public static final String PLUGIN_NAME = "TestPlugin";
@Override
public String getPluginName() {
return PLUGIN_NAME;
}
...
@Override
public boolean onExecute(String args) {
doSomething();
callbackContext.callback(...);
return true;
}
}
复制代码
其中,H5 容器和插件都具备 Activity 生命周期感知能力,插件的生命周期:
插件统一经过插件管理平台进行维护管理,目前已有200+插件。PG 插件做为基础通用功能,采起集中式管理机制,任何人在新增、修改插件都须要进行相关负责人审核,以免出现 Android、iOS 两端实现不统一,版本间实现不统一等问题。
插件调试经过调试平台进行操做,浏览器中打开调试地址,App 端经过调试工具扫码创建链接,便可进行插件调试。
Hybrid 混合开发的一大劣势就是性能比较差,打开页面较慢,特别是在弱网状况下。因为51信用卡业务大部分都是静态资源请求,参考业界作法,咱们实现了动态下发离线包的方式来提高H5页面打开速度。
这里细节问题不具体展开。
除了以上提到的实践外,咱们还作了不少工做,好比 UI 统1、Back 键拦截、公共参数处理、PG 白名单机制、H5监控、PG 方法监控等等,限于文章篇幅,这里再也不一一列出,敬请关注后续相关文章。
在 Hybrid 已有配套基础上,51信用卡选择了 Weex 做为跨平台方案,通过一年的踩坑填坑过程,目前已经有 20+ 个项目、数百个 Weex 页面在线上稳定运行,而且,目前 Weex 方案趋于成熟,已经做为51信用卡端内首选业务方案。
因为 Hybrid 良好的面向接口编程特性,在进行 Weex 基础建设过程当中,很方便的就把已有的插件集成进来,而且共享已沉淀的配套设施。
public class ENBridgeModule extends WXModule {
@JSMethod
public synchronized void send(String method, String args, JSCallback jsCallback) {
...
weexWebView = weexEngine.getWeexVirtualWebView();
EnNiuJsBridge enNiuJsBridge = weexWebView.getEnNiuJsBridge();
enNiuJsBridge.notify(pg);
}
}
复制代码
注册 Weex 的 Module,而且每一个 Weex Engine 中会新建出一个虚拟 webview,用于桥接 JsBridge 进而调用 PluginManager 进行插件逻辑分发。
Weex 容器实践在以前的文章中已经提到过一部分,具体请看 Weex避坑指南-理论篇 ,后续还将有 Weex 实践相关的文章放出,这里不作过多篇幅的介绍,敬请关注后续相关文章。
工程化本质上是为了提升研发效率。51信用卡客户端团队自研的大风车管理平台,用于 App 管理、持续集成、类库管理、发版管理等,围绕客户端研发上下游流程,创建统一的管理入口。
目前,51信用卡 iOS 和 Android 共 30 多个应用 App、 200 多个类库依托大风车平台进行管理。下面主要介绍下类库管理相关内容。
51信用卡目前有 100 多个 Android 类库,每一个类库对应一个独立的 Gitlab 仓库。过多的独立组件及独立仓库,管理起来有些麻烦。
依托于大风车平台,全部类库的名称、最新版本及标签类型都会展现在列表页,标签类型对应组件化架构的层次结构,包括:基础组件、单业务功能组件、多业务功能组件。
在类库详情页,会有库的功能描述、groupId:artifactId 依赖信息、版本历史记录、分支信息、README、CHANGELOG、负责人等详情信息。
全部的类库管理工做均可以在大风车完成,包括新建类库、类库发版、查阅相关信息等等,这大大提升了基础组的研发效率,下降了团队间的沟通成本。
而且 App 工程中,该 App 所依赖的全部类库信息一目了然,在多人维护、多类库并行开发、类库频繁发版的状况下,依赖类库信息 check 更加便捷。
因为类库之间是仓库隔离,因此它们的依赖关系是 maven 依赖,全部类库的 aar 包都须要发布到内部 maven 服务器上,上传工做由 PublishMavenPlugin 完成。
对于开发调试阶段,每一个类库自带 DemoApp 工程,因此采用源码依赖;开发完成后,类库使用SNAPSHOT
版本(好比 1.0.0-SNAPSHOT)发布到 maven 服务器,接入 App 工程后 push 代码触发大风车打包,进行集成测试。须要修改类库时,能够再重复发布相同版本的SNAPSHOT
版本。
SNAPSHOT
版本能够在开发同窗本身的机器上进行打包发布。
对于发布阶段,类库必须使用正式版本发布,因为正式版本不可重复发布,这也就要求开发同窗保证每一个正式版本的版本质量,在正式发布前都应达到发布标准。
因为类库内部也存在相互依赖的状况,因此在类库正式发布时,不容许依赖包含SNAPSHOT
版本的类库,DependencyCheck
工做也会在 PublishMavenPlugin 完成。
同时,正式版本不容许开发同窗在本机打包发布,PublishMavenPlugin 会检测是否在云端打包环境。功能分支经 CodeReview 后合并 master 分支,而后建立对应版本的 tag,触发大风车进行打包发布工做,发布成功后,会邮件通知 Android 组同窗,并附带 CHANGELOG。
App 工程下采用 compile 依赖,compile 会解析类库 maven 包中的 pom 文件,进而间接依赖 pom 文件中声明的其余类库,也就是依赖传递。正常状况下,依赖传递会减小没必要要的类库声明,当出现版本冲突时会自动处理 merge 操做。
可是,在多人协同工做、多类库并行开发状况下,事情变得有些复杂。考虑一种状况,应用 A 依赖类库 B,类库 B 依赖类库 C,正常状况下,A 中只须要声明依赖 B 便可,C 会被依赖传递过去。若是 C 中改变了方法签名,而且在应用 A 中显示声明依赖 C,编译时和运行时会分别出现什么状况?在编译时没有问题,正常编译经过;在运行时,当运行到类库 B 中使用的类库 C 中被改变签名的方法时,App crash。这是由于,maven 在处理类库版本 merge 时,会将 C 升级到最高版本,而此时 B 中已经编译好的 class 中使用的仍是老版本 C 中的方法。
为了处理这个问题,咱们使用 APICheckGradlePlugin 在编译时进行 check 操做,当发现被调用的方法找不到时,主动报错,将错误提早暴露在编译期,而非在运行时。同时内部强调 API 接口的向下兼容性,不用的方法标记为废弃,而非直接修改其方法签名或删除方法。
APICheckGradlePlugin 核心代码以下:
try {
c.getClassPool().get(callClassName)
isClassNotFound = false
m.getMethod()
} catch (NotFoundException e) {
if (isClassNotFound) {
dealException(String.format("在%s类中的第%d行是用到的%s类不存在", className, line, callClassName))
} else {
dealException(String.format("在%s类中的第%d行是用到的%s类的%s方法不存在", className, line, callClassName, methodName))
}
}
复制代码
上文中提到,在多业务组件库工程中会有多个 module,一个 api module,一个 imp module,在使用 DemoApp 编译调试时采用源码依赖, imp module 依赖 api module,App 依赖 imp module,这样在打包上传 maven 时,会出现没法一块儿上传的问题;而且咱们也要确保 api 和 imp 的版本号一致。为了解决这个问题,须要在上传时动态修改他们的 pom 文件,代码以下:
modifyPom { pom ->
pom.dependencies.findAll { dep -> dep.groupId == rootProject.name }.collect { dep ->
dep.groupId = pom.groupId = rootProject.groupId
dep.version = pom.version = rootProject.sdkVersion
}
}
复制代码
因为每一个新建组件类库的 App 工程须要运行时环境基本相同,包括网络环境、调试环境、gradle 配置、通用依赖配置等等,这些重复性的工做最好放在一块儿统一处理。为此,咱们建立了组件库的模板工程,只须要 clone 下来模板仓库,而后修改一些代码便可开发需求代码。
可是,这种方式依然有不少共性的工做,好比 clone 代码、修改类库名、修改 groupId:artifactId、建立新的类库仓库、push 代码、在大风车中新建类库关联仓库地址等等操做。这些共性操做仍然能够用机器来操做,因此咱们在大风车新建类库这一步中,把前面全部要作的事情所有作完,只须要在新建类库时填入必要的参数,一键就能够建立出可用的类库项目。
随着我司 App 愈来愈多,新建 App 的配置一样面临类库刚开始时的困扰,新建 App 与新建类库本质上是同样的,只不过所需参数更多一些,而且这些参数可能不固定,有些 App 须要有些 App 不须要。参考类库,咱们提取共性操做,建立了 App 的模板工程,而且对接大风车,一键便可建立出 App 工程,那些可变的参数留在模板工程中按需手动配置。
在组件化初步开始时,咱们的每一个模块都有固定的负责人,每一个人手上都有固定的若干个模块,责任人对本身负责的模块负责。
可是随着组内的人员变更和业务变更,致使一些模块频繁易主,一些模块的文档长期处于不被维护状态,README 和 CHANGELOG 常年失修。
依赖大风车的类库管理,从新为每一个模块指定负责人,而且梳理现存类库哪些缺失文档,进行补全。自从大风车自动抄送类库发版 CHANGELOG 后,CHANGELOG 不全的状况也大幅改善,基本每一个新的版本都会附上该版本所作修改。
同时,咱们也强调 CodeReview 机制,每一个模块在提测前进行 CodeReview,强制merge request 必须有人点赞后才能合并 master 分支等等代码审查机制。将来,咱们可能会进一步实践负责人 backup 方案,主副负责人相互 review,扩大你们技术视野的同时,能够进一步提升你们的主人翁意识。
好的架构不是设计出来的,而是演进出来的。本文简单阐述了51信用卡 Android 架构演进的一些实践经验,同时咱们坚信技术方案没有最优解,重要的是要选择选择适合本身的。脱离所处环境和问题自己谈技术方案,都将不能获得适合自身的开发架构。同时,咱们也应当吸收和借鉴业界优秀的架构和设计理念,并将其根据自身适用场景加以改造,在理论和实践中逐渐交替探索演进。
固然,咱们目前所使用的架构依然存在一些问题,好比组件拆分不彻底、主工程业务仍然不少、CodeReview 机制不健全、代码扫描不够严格、一些组件库没有严格按照 api 工程来改造、一些老的组件依然没有 api module等等问题。咱们也应该看到,正是由于这些实际的问题在推进咱们进行技术改造,架构升级。同时,咱们也要审视行业内大的方向,紧跟技术趋势,主动拥抱变化,毕竟技术世界惟一不变的,即是变化。