相关阅读:
吊炸天!74款APP完整源码!
2017年高频率的互联网校园招聘面试题
阿里两个最新Android开源库,Android开发者的福音
原文:http://blog.csdn.net/ahence/article/details/56678126
技术发展日新月异,业界各种Android客户端架构设计,五花八门,但我们不能简单地说哪种架构更好,因为脱离业务谈架构是没有任何意义的,适合业务的才是好架构。而架构也不是一成不变的,随着业务的发展,也许当初设计的架构已不足以支撑目前的业务,那么就需要改变之前的架构。接下来将分享下我们Android客户端的架构设计,在App的某个业务发展阶段或许有一些参考意义。
分层化与模块化
分层化与模块化应该是任何软件开发的共识。
分层化
在Android应用开发中通常可以分为如下几层:
SDK层:主要是Android SDK及第三方的SDK(可能基于Android SDK或为独立的SDK),这些SDK为上层框架提供核心功能的支持。
基础框架层:这里所谓的基础框架,指多数App都必需的基础功能,是具体业务逻辑实现的基础。主要有网络请求功能、图片加载与缓存功能、SQLite数据库管理功能、Log管理功能等,当然根据对业务逻辑支持的不同,基础框架层的功能支持也不一定相同,上述几个应该是大部分App都要支持的,当然Crash监控与常用工具类也可归为该层次。
具体到每个基础框架的实现则没有任何限制,如网络功能可以使用Volley、OkHttp或者自己封装实现网络请求逻辑;对于图片管理功能则可以使用Glide、Fresco、Picasso,亦或自己实现……总之每个基础框架都要遵循一定的实现原则,保持功能模块的独立性,与具体业务解耦并对外提供良好的交互接口。
业务逻辑层:如果把App架构比作高层建筑,那么上述两层就是地基。地基打好之后,就可以在上面任意发挥了,至于如何发挥,那就必须结合实际的业务需求,不同的应用往往有不同的业务功能模块。
另一方面,业务功能模块也并非完全是并列的级别,有一些业务逻辑也是可以抽象出来的,作为通用的功能模块,比如登录、分享、扫描、统计等,其他的业务模块可能会调用到这些功能。
这里需要注意的是SDK层与基础框架层并不是一成不变的,但它们的变化周期往往是比较长的,一般来说当基础功能不能满足最上层的业务逻辑时,就需要对其做扩展。由于基础框架层的功能模块已经是功能级别的粒度划分,因此扩展往往是模块级别的扩展,通常是新增基础功能框架而不是修改原有基础功能框架,这也符合“开放-闭合”原则。
模块化
至于模块化,对于分层化来说则是更细粒度的划分,即将每一层细分为不同的模块,各功能模块尽可能遵循“高内聚、低耦合”的原则,功能模块之间仅提供必要的交互接口。
业务数据流程设计
由于业务逻辑、数据处理逻辑或网络框架的不同,相信各家应用都有自己的一套数据请求流程。最直接的就是从Activity或Fragment中调用网络请求的方法,然后通过回调将结果返回到Activity或Fragment中,虽然流程最清晰,但这种方式存在几个严重的问题:
网络数据直接返回到Activity或Fragment中,后续需要对数据进行解析、过滤、转换、缓存等操作,这些工作将会大大加重Activity或Fragment的负担。
Activity或Fragment的代码量猛增,逻辑繁杂(不仅包含了View的逻辑还包含了数据处理的逻辑)
从整个应用的角度来看,每个页面甚至每个接口都需要重复上述相同的冗余工作,完全可以抽象出来。
上述设计思路是需要摒弃的,结合自身业务及架构演化,我们没有跟风MVP、MVVM,而是设计了下面一套业务数据请求流程:
首先,视图层通常表现为Activity或Fragment,并由视图层发起数据请求,与上述不同,视图层并不直接跟网络框架打交道,而是先将数据请求发送到数据代理层DataAgent。需要注意到是,视图层与数据代理层之间没有采用直接通信的方式,而是插入了一个消息调度器MessageScheduler中转。这样做的好处是将视图层与数据代理层解耦,视图层无需关注数据代理层的具体实现,有了MessageScheduler,视图层所要做的就是发出一个数据请求的消息而已,然后就可以静静等待一个回复消息,该回复消息会附带最终需要的数据对象,这样在视图层就免除了数据处理的逻辑,拿到结果直接展示到UI上即可。使用这种方式,一般来讲Activity或Fragment三五百行代码即可搞定,UI逻辑或接口逻辑(如一个页面有多个接口)比较复杂的代码量基本也能控制在1000行左右,逻辑非常清爽。
消息调度器将视图层的请求消息转发到数据代理层后,DataAgent解析出数据请求类型DataType(该类型对应着具体数据对象模型)、必要参数(接口参数、是否需要缓存结果、分页页码等),然后再执行具体的操作:
如果要取缓存的数据,则DataAgent直接向缓存模块发送请求。缓存的数据可以是初始JSON数据,也可以是解析处理后得到的数据对象Model,可根据具体需求配置。如果从缓存中取到的是JSON,则DataAgent先要解析处理得到对应Model;如果从缓存中取到的是Model,则不做处理,然后将Model封装发回到消息调度器,再由MessageScheduler分发给具体的请求者,如Activity或Fragment。
由于Android的数据来源有多种,如果数据来自持久化存储,如SQLite或File等,仍然统一由DataAgent来跟它们通信,获取数据并加工后通过MessageScheduler发回视图层。
最常见的是从服务器获取数据,此种场景下,DataAgent将与网络框架交互,将从MessageScheduler中获取的参数提供给网络框架构造请求url。至于网络框架使用Volley或OkHttp或者其他都没关系,网络框架负责向Server请求数据,数据通常以JSON格式返回。DataAgent收到返回的JSON数据后,根据DataType将JSON数据校验后抛给解析器,解析器会将JSON解析为视图层需要的Model。当然数据解析过程可能伴随数据的过滤、转换等逻辑。另外需要注意的是,还需要根据视图层需求对数据进行是否缓存的操作,可选择缓存JSON还是Model。经过一系列操作,得到最终Model后,DataAgent将其通过MessageScheduler发回视图层。
当然,由于数据请求流程是耗时的,因此上述步骤都是走的线程池,这点上图中并未注明。
数据代理层
DataAgent在上文中已简单提及,它的主要作用是对数据的一系列操作,包括实际的数据请求、数据解析处理、数据缓存等逻辑。下图为从服务端接口获取JSON数据并处理的流程:
从上图可知,DataAgent的大致工作流程为:
DataAgent将真正的数据请求发送给各数据源,数据源可能为缓存、SQLite或文件,但通常是从服务端获取数据,因此DataAgent会将数据请求发到网络框架层,然后等待数据返回。
由于数据源不同,返回数据也可能不同,这里简化为两种:原始JSON或Model。
DataAgent拿到数据后,则开始数据处理流程。以从网络请求的JSON数据为例,先对返回的JSON进行数据校验,检查数据的有效性与正确性,如果数据校验通过,接下来根据需求来决定要不要写入缓存,然后再进行数据加工(如精度处理、数据拼接、数据裁剪等),最后进行数据解析得到视图层需要的Model。如果数据校验没有通过,则尝试从缓存中读取,从缓存中读取后也需要校验(检查数据的时效性、有效性、正确性),校验通过后同样进行数据处理、解析等流程。如果缓存中读取得到的就是Model,那么则可以省略数据处理和解析的流程。得到最终的Model后,DataAgent将其包装发送给MessageScheduler。另外DataAgent还要具有一定的容错功能,因为任何数据源都无法保证能够返回合法的数据,如果不对数据错误进行容错处理,那么就可能无法解析为对应的Model,从而导致视图层无数据甚至异常。如果接口及缓存都无法返回正确的数据,DataAgent需要做特殊处理,以保证视图层能给用户以反馈。
业务视图逻辑
虽然不同的业务页面有不同的视图逻辑,这里以一个应用中最常见的页面为例来说明,假设该页面有一个列表。大家都知道ListView(此处为泛指,可能大家都在用RecyclerView了)的工作方式,它需要ViewHolder来填充视图,需要Adapter来填充数据,如果每个需要ListView的界面都维护各自的一套ViewHolder及Adapter,那么页面逻辑又将变得臃肿。
我们在实践中是这样做的:
经过上述封装之后,视图层只需要向Adapter公共处理类传入一个type参数即可得到对应的Adapter;等数据返回到视图层后,再将数据传给Adapter公共处理类,其他什么都不用管,就可以展示列表数据了。原本需要很多代码实现的逻辑从视图层抽离之后,视图层只需要几行代码就能够完成一个列表展示了。
Hybrid框架
自Android诞生以来,就有Native App与Web App之争,这两种开发方式虽然各有优缺点,但Native App一直占据上风。近一两年来,移动应用中的Web页面越来越多,而纯Native的应用则相对越来越少。但是纯Web App由于其渲染效率、性能问题、对硬件的调用限制导致其也并未广泛地应用。于是一种折中的方案成为主流,即Hybrid App。
所谓Hybrid App,即混合开发方式,部分功能使用Native开发,部分功能使用H5开发。为了充分利用Web开发的优点并避开其缺点,并非所有业务功能都适合使用Web方式来开发。在我们的应用中,主要将H5用于以下方面:
支持完整的Web页面,即整个页面的内容全部是H5实现,外部容器为Activity或Fragment。
支持局部的Web页面,即部分页面的内容是H5实现,可单独使用自定义的WebView或者嵌入Fragment使用。
定义了一套较为完整的交互协议,支持Native与JS的互相调用,典型的场景如H5页面点击跳转Native功能页面(支持传参)、JS唤起Native对话框或Toast等,同时Java也能调用JS函数。基于此套交互协议,基本能够满足日常App中Web开发需求。
避免了JS注入漏洞。
支持同一个Web页面中Http与Https混合的场景。
向业务逻辑层暴露接口,可根据需求定制WebViewClient与WebChromeClient。
对外提供接口,可根据需求控制缩放、Cookie管理、缓存管理、硬件加速等。
经过试验与摸索,兼容多种Android设备及版本。
虽然后来出现了React Native,但由于学习成本及其Android版本的局限性,结合我们自己团队的人力资源原因,我们尚未在应用中正式使用。目前仍然以Hybrid开发为主,且其在整个应用中的比重越来越大,因此Hybrid框架是我们架构中重要的一个组成部分。
消息调度中心
消息分发器
既然有了消息调度机制,就需要消息分发器MessageDispatcher,来负责发送消息。
MessageDispatcher本质上是利用了Android的消息机制来对业务需求进行封装和扩展。看过Android Framework层源码就会发现其实Android框架本身就有很多地方使用了消息机制来进行通信,Android消息机制可以在模块页面间、线程间通信,甚至可以在进程间使用Messenger通信(Messenger方式是利用了消息机制,当然还有其他进程间通信方式)。
MessageDispatcher功能比较简单,支持两种方式:
其示意图如下:
模块路由中心
一个完整的应用中,免不了模块之间、功能页面之间的跳转。当然在需要的地方通过Intent可以实现跳转,但这不是一个好的方案,很明显不同模块或页面之间的耦合度增加了。而我们的原则是模块和页面之间尽可能解耦,于是设计了一个模块路由(Module Routing)中心,App中所有的页面跳转均由其控制。
模块路由的核心原理是给功能页面进行唯一编码,编码的逻辑可以跟随产品版本定义到应用中,并保证兼容之前版本。这样就可以在应用的任何地方只需要向模块路由中心发送对应模块页面的编码即可,由模块路由负责打开目标页面。
以下几点需要注意:
使用模块路由的好处有:
大量减少应用中的跳转Intent
模块之间、页面之间解耦
适配变化,统一管理,修改方便
其他
日志系统
在开发过程中,甚至运行过程中,日志都是很重要的一部分。当然Android提供了Log相关的API,但不建议这一行那一行地零星使用,否则如果想统一控制Tag或关闭Log时非常麻烦。建议对Log API进行简单封装或者使用现有第三方Log库,将Log功能独立出来,提供统一的调用接口、级别控制、开关控制,这样既方便调试也方便管理,同时也能为整个应用代码的清晰做出一点贡献。
线上崩溃监控
统计系统
相信大部分应用都有统计分析后台,可以统计应用的日活、PV、UV或其他用户行为,也可能有一部分应用是使用的第三方统计功能,如友盟等。结合公司BI部门的统计需求,我们客户端自行设计了一套统计方案,用于Android与iOS两个客户端。之所以不用第三方统计,主要是因为我们无法根据需求自由定制且数据不在自家服务器,另一方面也有些许数据泄露的风险。
基于客户端的统计系统主要包括三个方面的功能:
域名劫持应对策略
应用预先内置IP。
每次启动应用时获取最新IP,并保存到应用本地。
请求数据时,先使用域名走正常的逻辑,一旦遇到疑似劫持的问题后,使用本地的IP进行直连尝试。
总结
上文提到的是我们Android应用架构中的核心部分,可能你发现并没有什么花哨的、潮流的玩意儿,没有MVP,没有RxAndroid,没有插件化,也没有热修复……但就是这样它仍然支撑起了上亿的用户量。世上没有完美的架构,只有符合自身业务的架构,上述架构还有很多缺点,我们也在有选择、有步骤地重构,而随着业务需求的扩展,架构也会不断演化,最后希望本文能给大家带来一点参考意义。
Java和Android大牛频道
欢迎关注我们,一起讨论技术,扫描和长按下方的二维码可快速关注我们。或搜索微信公众号:JANiubility。
公众号:JANiubility