搭建安卓应用架构的实践总结

近两年安卓开发社区提倡使用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-httpjava.net都存在,直到6.0的SDK里删除了apache-http。近几年,很多优秀的第三方网络库在安卓开源社区流行起来,例如Android-async-httpVolley(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与架构经验谈》详细对比了三种模式的相同和不同点

《依赖注入框架性能对比》详细对比了几个主流的依赖注入框架

写在最后

通篇看下来,其实说的都是很简单的理论和做法,没有复杂的模式和实现。其实“面向接口编程”也是大家都明白的道理,可是在做具体功能的时候容易只考虑快速实现,不去设计分层,习惯用一个类完成所有功能。随着项目业务逻辑增长,必然形成一个或多个超级类,必然形成模块的紧耦合。所以还是有必要强调面向接口设计这些基本编程原则。

“面向接口编程”,“单一职责”,“高内聚低耦合”等等原则是其它设计模式的出发点和实现目标,或者说原则是根基,具体的模式是表现形式。架构设计和重构的过程不是实现某个具体模式的过程,而是根据基本原则设选择一种合适的模式的过程。

最后还想说,每个项目都有自己的特点,引入新的设计模式或第三方库还需对症下药,盲目使用有害而无益。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容