近两年安卓开发社区提倡使用MVP或MVVM替代MVC,近期谷歌官方还在Github上公布了示例代码供大家参考。另外,以Square公司开源的OkHttp为代表的一批优秀的第三方库被越来越多的开发者接受和使用,极大节省了开发者的开发成本。对于项目来说,这两个并非毫无关系。模式是框架,第三方库是实现具体功能的;使用第三方库的时候,不仅考虑如何实现功能,二要考虑在哪个模块和逻辑层次上去实现,如何能适应设计变动,这就跟具体的框架有关;而且有些第三方库也能辅助设计框架的实现。
新项目启动之初,我们借鉴了安卓开源社区的经验,总结了自己的一套方法。以下内容先从设计模式出发,阐述了为什么在安卓开中要从MVC转向MVP以及如何时使用MVP;其次介绍了几个优秀的第三方库,以及如何使用他们来辅助MVP的实施。最后总结了这种方法的使用成本。
对于全新项目,希望这篇文章能为你提供项目架构模型;对于有计划做架构调整的项目,希望这篇文章能为你提供有益的参考。
示例代码在github上 StartupArch (Model部分尚未完成)
架构设计目标
对于架构设计这四个字,我的理解是:结合项目本身的特点,提出设计目标和设计原则,具体的实施方法(或工具),这样的一个过程就是架构设计。按照以上几个关键因素来展开说明。
架构设计的四个目标:
低耦合:模块间低耦合。模块间耦合性太强就容易出bug,有了bug也难修复,更别提代码复用了。
高内聚:模块内高内聚。将逻辑实现封装在模块内,修改功能或修复bug时代码的修改范围也较小。
模块化:根据业务逻辑划分功能模块,模块间通过接口进行交互,有利于模块间解耦;同时,对于安卓App来说,模块化也是实现插件化等动态更新功能的前提,否则动态更新实现起来比较困难。
易复用:复用有很多种,根据代码抽象级别可以划分为函数、类、库复用,也有设计思想、设计模式复用等。业务逻辑和功能的复用也是很经常的。耦合性太强的代码是没法复用的。
以下内容将论述如何实现以上四个目标。
从MVC到MVP
MVC(Model-View-Controller)是一种设计模式,也是一种解决问题的方法和思路;MVC的意义在于指导开发者将数据层(M)与展现层(V)解耦,提高代码,特别是模型部分代码的复用性。
绝大部分安卓应用是基于MVC模式开发的,那么M、V、C在Android Framework中分别对应那些系统组件?日常开发中有事如何使用MVC的呢?
错误的MVC用法
看下图:日常开发中是如何使用MVC的,常用的安卓四大组件属于哪个模块。
MVC组件和调用关系通过上图我们发现至少有几个问题:
1,View和Model间的调用关系比较复杂,无法做到解耦。
2,由于Activity有双重属性,既是View又是Controller。Android中Activity代表一个页面,所以它是View;同时Activity的生命周期回调函数和响应用户事件(因为setContentView()后就需要设置控件的响应函数等),这些特点使得它又是Controller。
3,也因为具有双重属性,容易造成Activity变得非常庞大、代码特别多。
Activity(加上Service,BroadcastReceiver)几乎囊括了安卓应用的所有业务逻辑入口,而我们开发时为了省事儿就在这些入口处直接编码实现功能,且不做模块分离,于是这些类就随着功能越来越多而变得庞大无比,上万行代码的Activity或Service就出现了;紧耦合、强依赖各种问题伴随而来;如果不能及时拆分模块、解耦、梳理依赖关系,bug就越来越难解决。
总结一下:Android Framework没有给出明确的逻辑分层和解耦的意图,同时项目缺乏架构设计规范或者没有约束程序员的编码随意性,导致了复杂调用关系或庞大的Controller类的产生,导致了高耦合性的模块。
使用MVP
说完MVC再来看看MVP(Model-View-Presenter)。
Presenter:业务逻辑的实现;跟View通过接口来实现交互;直接调用Model模块来查询和存储数据,通过同步或异步的方式来接受结果;
View:负责UI显示和用户交互功能,使用Fragment或自定义View类来实现(Fragment的使用存在很大争议)中实现(不要在Activity或其它业务逻辑口类中实现);通过Presenter来获取和存储数据,不能跟Model直接发生调用关系;
Model:跟MVC中的基本类似,后边有说明。
相对于MVC,在安卓开发中使用MVP有个很大的好处,就是P(Presenter)代表业务逻辑,指向性非常明确,只对业务逻辑进行封装,跟Controller区别大,跟View的区别更大,所以使用时不存在模糊的情况。说的具体一点,就是编码时能望文生义,不会把UI有关的的代码都往Presenter里堆(别小看这一点,设计模式从某种程度上说也是一种代码规范,能减轻阅读别人代码的时间成本)。
通过上边的解释,应该可以得出以下使用原则:
1,解除View和Model的依赖关系,他们之间不再有直接的调用关系;
2,View和Model之间数据传递只能通过Presenter来完成;
3,View和Presenter通过接口进行调用,方便功能扩展和复用;
4,每个功能模块中View和Presenter是一对一关系,如果功能复杂可以考虑使用多个Presenter;
5,Presenter的复用性增加,因为往往多个页面使用相同或相似的业务逻辑,页面经常变而业务逻辑变化不大;
6,Activity等业务入口类只负责构建View和Presenter的依赖关系,不负责具体的UI显示和用户交互,以此防止过于庞大(其实代码量还是不小);
7,Activity类要继承一个父类,例如BaseActivity,将一些公共功能放在父类中,例如主题设置,Activity切换效果,登陆状态的查询等;这也是减小Activity代码量的方法之一;
8,对于回调,例如Presenter回调View,Model回调Presenter,大多是异步回调,可选的实现方案在下边的异步通讯中讨论;
按照以上原则得出的架构示意图如下:
Model-View-Presenter组织架构图需要说明一下Model模块:上图中的Model模块是针对单个功能模块的,因为每个功能使用的数据类型一般不变;每个DataAccess控制类决定使用内存数据还是磁盘数据,以及是否使用缓存。另外,缓存的设计都有比较成熟的技术方案,例如Android自带的LRUCache可以用在一级和二级缓存上,针对不同的领域也有很多第三方库(例如Picasso解决图片的缓存)。因此,团队可以在横向上/不同模块间采用统一的缓存方案。
辅助实施MVP的工具(第三方库)
1,依赖注入
当一个类的实例需要另一个类的实例协助时,通常有调用者来创建被调用者的实例。然而采用依赖注入(Dependency Injection)的方式,创建被调用者的工作不再由调用者来完成,因此叫控制反转(Inversion of Control),创建被调用者的实例的工作由IOC容器来完成,然后注入调用者,因此也称为依赖注入。
控制反转的基础是面向接口编程,但是即使是做到了面向接口编程也未必能避免硬初始化(Hard Inition)问题。硬初始化带来诸多问题,使得代码移植或复用变得困难,不能灵活的应对单元测试。使用依赖注入能方便的消除硬初始化。
使用依赖注入的好处:
1、依赖的注入和配置独立于组件之外,注入的对象在一个独立、不耦合的地方初始化,这样在改变注入对象时,我们只需要修改对象的实现方法,而不用大改代码库。
2、依赖可以注入到一个组件中:我们可以注入这些依赖的模拟实现,这样使得测试更加简单。
3、App中的组件不需要知道有关实例创建和生命周期的任何事情,这些由我们的依赖注入框架管理的。
引入Dagger2
Dagger 2 是 Square 的 Dagger 分支,是一种依赖注入框架,目前是第2版,由 Google 接手进行开发。Dagger2把项目模块化,形成一个解耦的组件图,并且使每个模块方便的获取自己依赖的对象。Dagger2是使用代码自动生成和手写代码来实现依赖注入,并非基于低效率的反射。Dagger2自动生成中间类代码使得调试的容易,也方便大家了解到实现原理。
使用Dagger2
详细的使用方法可以看扩展阅读部分,下边只讲使用原则。
Dagger2的使用原则和方法上图说明Dagger2的使用方法:对于Application级别的公用组件(或功能,例如ApplicationContext,设备信息,工具类等)可以封装到顶级Component中,每个Activity将其作用域的组件(或功能,例如实现业务逻辑的Presenter)封装到子Component中。看到Activity Component和@PerActivity,应该要理解到这层意思:对于其他组件,例如BroadcastReceiver,也应该有对应的Component和注解(例如@PerBroadcastReceiver);Service的处理也是如此。
不得不承认,Dagger2在国内互联网公司没有流行起来,一个很重要的原因它的学习成本较高、学习曲线较陡峭。但是Dagger2作为依赖注解的优秀框架,确实能在代码解耦起到很大的作用,也能让代码变得更优雅。
2,网络模块
一直以来,在Android SDK中apache-http和java.net都存在,直到6.0的SDK里删除了apache-http。近几年,很多优秀的第三方网络库在安卓开源社区流行起来,例如Android-async-http,Volley(2013 Google IO),OKHttp等。
MV*中M端是数据访问层,其中网络模块是其最重要的一部分。因为第三方库众多,可选择的余地也比较大,因此我们主要考虑的是代码稳定性/使用者多少/支持的功能多少/安全性/编码量等因素。
采用OKHttp +Retrofit + JSON(or GSON)的方案,对该方案的介绍和跟其它主流技术方案的比较如下:
OKHttp
OKHttp是Android版Http客户端。非常高效,支持SPDY、连接池、GZIP和 HTTP 缓存。默认情况下,OKHttp会自动处理常见的网络问题,像二次连接、SSL的握手问题。如果你的应用程序中集成了OKHttp,Retrofit默认会使用OKHttp处理其他网络层请求。负责传输层的实现。
Retrofit
Retrofit是一个REST(代表性状态传输,Representational State Transfer)客户端,支持Restful风格的网络请求。能帮助用户处理各种类型的网络请求,帮助用户方便的创建请求接口/自动创建实现类(使用Java动态代理生成自定义接口的代理类)。从2.0开始它的功能更加专注,完全使用OkHttp作为传输层客户端;数据解析功能也完全由OkHttp来负责。
技术方案比较
网络模块技术方案比较3,异步通信
从上图MVC或MVP模式也可以看出来,绝大多数回调功能都是异步通信,尤其是在Presenter跟Model模块的交互过程中,因为大部分数据都是通过网络或磁盘存储。不同解决方案对代码的影响非常大,尤其是在模块解耦方面。使用接口比通过对象直接回调的方式要好,因为接口方便扩展;使用事件总线比使用接口要好,因为事件的生产和发送双方都不要持有对象。
这里讨论了安卓开发中常用的几种异步通讯方法。
Java提供的:Interface接口,Java版观察者模式等
Android提供的:Handler+Message, AsyncTask, Intent, BroadcastReceiver等
第三方库:EventBus,RxJava/RxAndroid等
需要说明一下,这里讨论的异步通信主要是用在应用内模块间的解耦。Intent和BroadcastReceiver主要是在组件级别的解耦,如果用他们来做例如网络请求回调这种功能则有浪费资源之嫌。
事件总线
EventBus,是一个Android端优化的publish/subscribe消息总线,简化了应用程序内各组件间、组件与后台线程间的通信。比如请求网络,等网络返回时通过Handler或Broadcast通知UI,两个Fragment之间需要通过Listener通信,这些需求都可以通过EventBus实现。
作为一个消息总线,有三个主要的元素:
- Event:事件
- Subscriber:事件订阅者,接收特定的事件
- Publisher:事件发布者,用于通知Subscriber有事件发生
使用起来很简单,自定义消息,监听消息,发布消息三部即可。比较受欢迎的事件总线库有greenrobot,otto,Guava等。
但是EventBus最终没流行起来,一个重要的原因是遇到了RxJava。
RxJava/RxAndroid
RxJava是由Netflix开发的响应式扩展(Reactive Extensions)的Java实现,它的异步实现是通过一种扩展的观察者模式来实现的。引用MSDN上对它的定义,Reactive Extensions是这样一个第三方库:它结合了可观察集合和LINQ式查询以达到异步和基于事件的编程效果。
RxAndroid是RxJava的一个针对Android平台的扩展,主要用于 Android 开发。它提供了例如AndroidSchedulers,针对Android的线程系统的调度器,方便使用者在主线程和子线程上分配任务,而无需自己去定义Handle/Message,极大简化了编码。
RxJava/RxAndroid能替代EventBus或AsyncTask,除了它实现了异步通信功能之外,更重要原因是用它实现的代码简洁,能把复杂的逻辑串成一条线。有兴趣的同学可以继续看扩展阅读里的文章。
4,数据库
在数据库方面只做原生技术和ORM第三方库的比较。因为我在项目中用到的都是使用sqlite和SqlHelper等比较基础的库,第三方ORM较少使用,所以简单说说。
ORM
对象关系映射(Object-Relational-Mapping),用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。安卓开发中常用的有GreenDAO/ORMLite等。
ContentProvider vs ORMs 技术方案比较
数据库模块技术方案比较使用成本
这里说一下使用这么多第三方库的几个坏处:
(1) 学习成本。我想这是很多项目不愿因引入第三方库的最、最、最重要的原因。Dagger2的学习成本尤其高,学习曲线尤其陡峭。
(2) 增大App的方法数。虽然现在安卓支持multi-dex,方法数大增还是会带来很多问题,例如安装包变大,次dex变多和包含文件的随机性(当然有技术手段可以处理,实现起来比较麻烦)等。
(3) 项目对第三方库的功能依赖比较强。依赖第三方库的功能实现,也受其bug的影响(直接引入源码能减少这种影响)。
(4) 一些意想不到的问题。例如我曾经在项目中引入了一个第三方库,结果导致无法生成混淆的安装包。花了几个小时才搞明白原来是该库有个依赖库没有在混淆文件中keep,导致class文件重复。
(5) 性能问题。凡事有利有弊,项目开始前需要技术负责人去权衡利弊。但是对性能要求高也不意味着完全放弃这些设计模式和第三方库,可以在部分模块中使用这些,或者裁剪第三方库。
这些都是使(爱)用(的)成(代)本(价)!
扩展阅读
《TODO-MVP》Google官方的MVP最佳实践代码
《Tasting Dagger 2 on Android》某github大神的文章,很好的阐述了依赖注解以及Dagger2的思想(中文翻译有些地方不准确)
《给 Android 开发者的 RxJava 详解》作者扔物线是国内较早的推广RxJava/RxAndroid的安卓开发者
《被误解的MVC和被神化的MVVM》讨论iOS开发中的庞大Controller问题,跟Android中的问题也很类似,作者唐巧,国内著名的iOS开发
《Android App的设计架构:MVC,MVP,MVVM与架构经验谈》详细对比了三种模式的相同和不同点
《依赖注入框架性能对比》详细对比了几个主流的依赖注入框架
写在最后
通篇看下来,其实说的都是很简单的理论和做法,没有复杂的模式和实现。其实“面向接口编程”也是大家都明白的道理,可是在做具体功能的时候容易只考虑快速实现,不去设计分层,习惯用一个类完成所有功能。随着项目业务逻辑增长,必然形成一个或多个超级类,必然形成模块的紧耦合。所以还是有必要强调面向接口设计这些基本编程原则。
“面向接口编程”,“单一职责”,“高内聚低耦合”等等原则是其它设计模式的出发点和实现目标,或者说原则是根基,具体的模式是表现形式。架构设计和重构的过程不是实现某个具体模式的过程,而是根据基本原则设选择一种合适的模式的过程。
最后还想说,每个项目都有自己的特点,引入新的设计模式或第三方库还需对症下药,盲目使用有害而无益。
网友评论