移动客户端架构案例分析与思考
移动客户端架构案例分析与思考
写在前面
关于题目
分享之前,想说一下为什么选择了“架构”这个主题,其实初衷有两个:
第一,“架构”对于我们来说实在是太重要了,咱们虽然没有架构师这个职位,但是在开发的时候,都需要先有个很好的设计,希望我们的代码是易维护的,而“设计”往往都会落到“架构”上。所以希望这次分享能够对于大家在架构设计上有一点帮助。
第二,即便“架构”如此的重要,大家再聊到“架构”这个话题的时候,还是感觉到有点“虚”。想想原因,可能是因为每个人对于“架构”的理解可能都不太一样,一个人在不同阶段对于“架构”的理解也会不一样,架构设计还很依赖于实践和经验,很多设计细节(取舍)都是在实践中不停的迭代、改进,进而反思才能得到价值观的升级。所以我借这次机会,也将我自己之前零散的架构方面的理解,总结一下,争取能形成一点体系上的认识。希望大家聊起架构时,能稍微不那么“虚”。
分享形式
为了不那么枯燥,今天分享形式,我选了3个架构的案例,来进行分析,来试图讲清楚我对于架构的认识,以及怎么样设计出一个好的架构,当然这个话题太大了,我今天先给大家开个头,希望先能有一点感觉就好。
最后希望大家带着批判的精神来听,欢迎多交流。
第一个案例:猎户语音iOS SDK —— 架构是被业务驱动的
第一个案例,举一个我们自己的,放在第一个来讲,也是想强调“懂业务”对于设计出好的架构来说,是非常重要的,这也是一些人往往会忽略的点。
很多同学肯定想,在项目的一开始,就设计一个完美的架构,以后能 hold 住各种变化。其实这是不可能的,架构一定是随着业务的变化,而不停的演化和进化的,在有的阶段还可能对于之前架构做比较大的调整。
背景
大家都知道,我们同时维护了几个App,比如小豹、小雅、锤子等,这些上层App,都要依赖于底层的一些framework,比如
- 业务中心 OrionServiceSDK.framework (包括很多主要业务)
- 信息流 OVSChat.framework
- 技能商店 OVSSkillStore.framework
- 声纹 OVSVoicePrint.framework
- 等等
在早些的时候,我们对于 SDK 的拆分粒度比较细,比如一个“找手机”技能,都会做成一个 framework。当时的 framework,应该在20个左右,各个 framework(组件) 的依赖关系图如下:
历史原因
我们这么做当时的原因很简单,希望能够解耦各个组件,将组件尽量拆细,然后App方想使用什么功能都可以做到热插拔,不多也不少,多好~
理想 VS 现实
理想和现实往往会出现矛盾,到实际应用的时候,这样的设计就给我们带来了问题。如果对每个 framework 进行编号,比如1到20,那么我们理想中是这样的:
但是现实其实是这样的:
对比两张图的含义就是,实现中我们各个App,使用的 framework 大体相同,我们即便把业务拆的很细,若干个被拆分的 framework 其实还总是绑在一起使用。并且,还给我们维护带来了巨大的问题,光每次打Git tag,都要折腾一会,会感觉精力都花在了一起辅助工作上。
优化
针对这个问题,我们专门优化了 framework 的个数,将相似业务的 framework 进行了合并,最终 framework 的数量减少到了10个一下,组件之间的依赖图变成这样:
优化之后效果也很明显,我们对于各个framework的维护,变得简单多了。
两个彩蛋
另外在优化的过程,有两个值得一提的是:
- 之前“信息流“和”推送“还存在环状依赖的问题,就会导致当你想把 ”推送“的framework合并到业务中心的时候,竟然还得让业务中心去依赖信息流,这个当然是无法忍受的,解决办法也比较经典,就是让依赖下沉,把 Push 依赖的信息流的协议,放到了业务中心中。
- 这次的优化,其实是将”松散“变成了”耦合“,和我们平时常提到的观点刚好相反,但是确实是我们当前甚至今后一段时间内做所以我想说的是,”耦合“其实只是一个特征,虽然大部分情况是缺陷的特征,但是当耦合成为需求的时候,耦合就不是缺陷了。(有没有一点在哪里听过的感觉)
小结
我们谈”架构“的时候,说的最多的就是”取舍“,什么叫”取舍“,就是说你不能很简单的就判别出哪个是好的,哪个是不好的,总是觉得有点左右为难。而如何取舍?业务就是非常重要的一个标杆,只有结合业务,才能判断出哪个是最适合自己的。我们结合了业务,对自己的 framework 的数量进行了精简,当然也可能会根据业务的变化,在未来某天,需要将现有的framework拆分的更细。
第二个案例:饿了么移动APP的架构演进 —— 形成体系的认识
你做的项目,技术架构是怎么样的?
几乎所有人在被面试或者面试别人的的时候,都会(被)问到这个问题,很多人会回答,我们架构是MVC(MVVM),少数人还会使用MVP或者VIPER,我们姑且都称为MV(X),但是真的架构仅仅就是MV(X)吗?其实我觉得MV(X)虽然是架构中比较重要的部分,但是还是远远不能说架构 = MV(X)。
为什么呢?带着这个问题,我们来看第二个例子,在这个案例中,我们关注下面几点:
- 架构是如何随着业务的变化而变化的(这个也是对上面观点的一个证实)。
- 我们谈到架构就提的 MV(X),处于架构中的哪个部分。
- 通过”饿了么“的架构演变,体会一下每个阶段的侧重点是什么,对于架构有一个体系上的认识。
文章地址:
饿了么移动APP的架构演进
https://www.jianshu.com/p/2141fb0dc62c
”饿了么“的架构经历了4个阶段的演化:
- 第一阶段 MVC
- 第二阶段 Module Decoupled (组件化)
- 第三阶段 Hybrid
- 第四阶段 React-Native & Hot Patch
第一阶段 MVC
这个古老而经典的模式,不用多说。它是一个软件”从无到有“,”短平快“开发的首选。也是大部分规模比较小的 App 几乎大部分时间精力都会与之打交道的一个架构,以至于人们提架构比弹MVC。
当然这个架构随着业务的剧增,很快就会出现弊端,朝着Massive-View-Controller的方向奔去。
第二阶段 Module Decoupled
随着代码量不断增加,功能模块越来越多,不管是分工开发协作,还是已有模块的复用和维护,组件化都成了这个阶段的重点。组件化有个两个关键:
- 如何划分组件。
- 如何实现组件之间的通信。
对于第一个问题,”饿了么“采用的方案,基本是业界广为使用的分类方案,将组件分为共有组件和业务组件,
- 公共组件提供了一些业务无关的基础服务:比如网络库、数据库、JSONModel等
- 业务组件则对应具体的一块业务,比如登录业务组件,订单组件等
对于两种组件的管理:
- 对于公共组件,使用CocoaPods进行版本管理(这点和我们目前不太一样,因为我们是SDK提供方,我们引用的第三方库,不确定我们的SDK使用方是否使用,是否更改源码,所以我们的方式,是将稳定版本的源码,混淆后打包进我们的代码)。
- 对于业务组件,这个和我们大体类似,采取了业务模块注册机制的方式来达到解耦的目的,每个业务模块对外提供相应的业务接口,再启动时向一个中心注册自己的Scheme(我们是协议)。
而在具体某个业务组件内部,则可以根据不同开发人员,不同队伍的偏好,来实现不同的代码架构,比如MVC、MVVM、MVP等,也都不会影响整体系统架构。
这时的架构图,看上去长这样:
我们可以看到,MV(X) 已经不是关注的全部了,很多模块已经和 MV(X) 不怎么搭边了。
所以说,架构不等于 MV(X),其实 MV(X) 关注的只是”应用层“的部分。
关于分层:
一般的,可以将App分为三层:应用层、service层、data access层。
- 应用层 是直接和用户打交道的部分,我们常用到的 UIViewController,Android的 Activity,负责了数据的展示、流向、用户交互的处理。
- service 层 是在应用层的下面,为应用层服务器的,对于应用层来说就像一个API调用延迟为0ms的Server API。一般会放在应用层的代码:网络接口调用、公共系统服务API(GPS定位、隐私权限访问)、一些 UTil 代码(所以我觉得比如一个 UIViewController 的一些私有方法和一些提工具性质的category,其实应该算serveice 层)。
- data access 提供和对于数据的”增删改查“的接口层。
第三阶段 Hybrid
业务的变化又来啦,当用户规模达到比较大的数量,这次不仅仅是功能的增加,每两周一版已经满足不了产品、运营躁动的心了;同时,用纯 Native 代码编写的 App,如果上线后有错误,只能等下一次提交市场。在如今互联网竞争如此激烈的时代,一次线上错误有时也会带来很大的影响。所以这时候,很多纯粹展示性的模块会使用 H5 的方式来实现。
但是这种方式也有它的弊端:
- 每次加载页面需要请求服务器,渲染时间比较长。
- 调用本地硬件设备存在一定的不便。
对于这个问题,也有很多方案可以权衡,比如可以提前将网页打包好,以减少网络传输的时间,同时提供一系列的插件来访问本地的硬件设备。
“饿了么”这里的做法是,综合了 Native 和 H5 的优缺点,将页面做了一个划分,纯粹展示性的模块使用 H5;而更多的数据操作、动画渲染性的模块使用 Native。
架构图长成这样子:
业务再一次再架构的演化中扮演了重要的角色。
第四阶段 React-Native & Hot Patch
又要频繁迭代,又要用户体验,这时就考虑到了RN;另外,饿了么这个阶段用户已经过亿,线上一个小 bug 都可能影响几万人的使用,所以这个阶段,重点在于 RN 模块的引入,以及 Hot Patch 热修复功能的引入。
在 RN 的使用方面,依然有一个取舍,要回答下面的问题:
- 哪些页面使用 RN,哪些页面不用 RN。
- 是整个模块使用 RN,还是一个模块的部分页面使用 RN。
- RN 和 Native 页面是2选1的关系,还是说是一个备份。
- RN 和 Native 页面如何通信。
“饿了么”的做法是:对于20%最重要的页面,做了一个 RN 的镜像,也就是一个备份,然后通过服务器的配置,来切换Native 还是 RN,这样如果 Native 页面出现问题的时候,先通过开关将线上的页面切换成 RN,先保证线上正常使用,然后使用 Hot Patch 完成修补后,再切换回 Native App 原生页面。
这时的架构图:
不得不说,这种做法不一定适合别的团队,毕竟一个页面,要写 Native 、 RN 两套代码,并且要一直维护,花的代价都有点大,不是每个团队都有精力去这么搞的。其实这点,也正说明了,你需要根据自己业务,设计出一个最适合自己项目的架构。
小结
小结一下:
- 业务一直在影响架构的变迁。
- MV(X) 其实只是“应用层”的事,对于架构应该有个系统的认识。
- 架构的设计,并不是有现成的拿来用就 OK 的事,还有很多细节的部分需要做取舍,依赖业务需求和经验。
第三个案例:《猿题库 iOS 客户端架构设计》—— 好的架构具有哪些特质
第三个案例我们回归 MV(X),毕竟它确实是我们日常开发接触比较多的一部分。
对了这个案例,想关注的“点”是
- MVC 和 MVVM 的优缺点。
- 如何能够规避缺点,结合优点,改进架构,设计一个适合自己的MV(X)架构。
- 这个思想的底层原理是什么,在别的场景下的设计能够通用。
文章地址:
猿题库 iOS 客户端架构设计
http://gracelancy.com/blog/2016/01/06/ape-ios-arch-design/
MVC
优点:
- 易理解,对应现实生活中也是这样的。
- 易上手,iOS、Android 默认就是个 MVC 的环境。
缺点:
- 当指责不是那么明确,不知道该放哪时,代码就会被放在"Controller"里面吧,Controller越来越难维护。
其实对于上面这个缺点,唐巧也在一篇文章中写道,这个问题其实也不能说是 MVC 的缺点,是我们没有拆分好代码。可以看看唐巧的《被误解的 MVC 和被神化的 MVVM》,提出了一些如何解决 Controler 臃肿的解决办法,然后也表达了对于 MVVM 的质疑,具体的做法可以去读这篇文章。这也正说明了大家对于架构的理解和态度真的是有区别的。
MVVM
具体关于MVVM的概念可以参考 Objc 的《MVVM 介绍》,这里就不具体说 MVVM 的概念了。
不了解MVVM的同学,知道这几点就行:
- MVVM将ViewController视作View。
- 关于 View Model,只需要知道两件事:持有model;View可以完全通过一个View Model决定自己如何展示。
- View 和 View Model,View Model 和 Model之间通过数据绑定,使得 Model改变的时候,能同步更新 View Model,进而更新 View。
优点:
- 减轻了 Controller 的负担,拆分了代码
- View Model有比较好的测试性。
- 结合 RAC, 可以将数据和 View 通信的代码精简到很少。
缺点:
- 上手成本高。
- 由于使用数据绑定,界面的 bug 变的不易调试。
- ViewModel 接管了 ViewController 的大部分职责,慢慢也可能变的臃肿。
综合两者
来看下 Lancy 的设计,是如何将两者综合,规避缺点,保留优点的,先上图:
对于上图的说明:
- 一个View Controller 持有一个 Data Controller。
- Data Controller,是数据管理模块,负责数据的生命周期:获取、保存、更新。
- 一个 View Controller 里面有多个 View,每个 View 对应一个 View Model,这里的 View Model 概念和 MVVM 里的类似,唯一不同的是这里的 View Model 和 Model ,没有绑定机制。
- View 的展示样式,完全决定于 View Model。
结合产品 UI,再按照数据流的方式阐述,以下面的 CollectionView 为例。
- View Controller 持有 一个 Data Controller,初始化之后,调用 Data Controller 获取用户打开的课程。(1)
- Data Controller 通过 API 获取数据,封装成 Model 并返回 (2,3)
- View Controller 将2中返回的数据,生成 View Model,调用 View 的 bindDataWithViewModel 方法装配给对应的 View。(4)
- View Controller 会调用 View 的渲染方法,View 通过 View model 直接进行渲染。(5)
- 如果有用户事件,通过代理的方式,传递给 View Controller,让View Controller 来决定下一步的处理。(6)
这么方式的优缺点:
优点:
- 指责分明,确定给 Controller 肩负。
- 耦合度低,测试性高。指责分明带来的效果就是耦合度低,同一个功能,可以分别由不同的开发人员分别进行开发界面和逻辑,只需要确立好接口即可。
- 学习成本低,不用事件绑定,不需要学习 RAC。
- 易于调试 Bug,不使用绑定带来的好处。
缺点:
- 当页面的交互逻辑非常多时,需要频繁的在 DC-VC-VM 里来回传递信息,造成了大量胶水代码。
- 没有绑定,带来额外的代码(绑定真的是双刃剑)
小结
针对这个案例,我觉得最应该我们思考的就是,作者 Lancy,在设计架构的时候的思路是怎么样的?为什么要那么设计?是怎么取舍的?总结一下:
- 因为想让团队能够快速上手,以及bug可以快速调试,所以没有使用绑定机制。(从学习成本、开发成本、以调试角度)
- 依然保证了指责的明确划分。(好的架构一定要明确划分职责,甚至均衡的划分)
- 方便测试依然是重要的一个设计标准。(好的架构要易于测试)
- 还有相当重要的一个标准——解耦(好的架构要易于维护,解耦意味着比较易于维护)
- 上面提到了缺点之一是“当页面交互逻辑非常多时,会不太合适”,这也说明了,作者采用了这个架构,其实是基于页面交互不是很多的情况(用户交互确实带来Model的改动不是很多,当前界面并不能修改用户所开的课程)。所以业务依然是影响架构设计的总要因素。
- 还有一点不知道大家有没有在意,上面提到了“数据流”,对着这个架构我们能清晰的说出“数据流”,这个我认为也是一个好的架构应该具有的特性。数据流如果很模糊,有很多分支,那我们的维护成本将大大增加,一个清晰的数据流,意味着你无论在这个流的那个节点继续执行下需,都能得到正确的结果。
基于对这个案例的分析,最应该思考的是,设计一个架构的思路,换言之,你要心里明白,怎样才是一个好的架构。
总结
总结一下,今天说的这三个案例,其实就是为了说明一下几点:
- 懂业务对于架构的重要性。
- 架构 != MV(X),站在更加宏观的角度看问题,对于打开思路更有帮助。
- 当我们设计架构的时候,怎样才是一个好的架构。
其实“架构”真的是个很大的话题,很多知识都可以拿出来单独学习和分享。
- 设计原则和设计模式(设计的基本功)
- 数据结构和算法(设计的基本功)
- MV(X)/Viper
- 组件化 (光这个就特别多可以讲的)
- 网络层的架构设计(比如离散的还是集约的)
- 持久化层的设计
- Hybrid 的设计
- RN、Hot Patch
- 无埋点
- pod 私有库维护 SDK
- 面向过程/面向对象
- AOP
- 。。。。。
希望以后能陆续的为大家分享,擅长哪个方向,或者对哪个方向感兴趣的小伙伴也可以给大家分享一下,让大家的设计能力一点点提高上来。
参考
MVVM 介绍
https://objccn.io/issue-13-1/
被误解的 MVC 和被神化的 MVVM
http://blog.devtang.com/2015/11/02/mvc-and-mvvm/
iOS应用架构谈系列
https://casatwy.com/iosying-yong-jia-gou-tan-kai-pian.html
猿题库 iOS 客户端架构设计
http://gracelancy.com/blog/2016/01/06/ape-ios-arch-design/
饿了么移动APP的架构演进
https://www.jianshu.com/p/2141fb0dc62c
iOS应用层架构之CDD
http://mrpeak.cn/blog/cdd/
iOS应用架构现状分析
http://mrpeak.cn/blog/ios-arch/
iOS 架构模式–解密 MVC,MVP,MVVM以及VIPER架构
http://www.cocoachina.com/ios/20160108/14916.html