今天给大家分享的是英语流利说Android端的代码架构的演进,标题挺高大上的,其实也并非多高大上的东西,整个演进过程,也是借鉴了业界很多大型应用在架构上的沉淀以及思想,可能有些东西还有点老生常谈,不过我们保证尽量都是干货。
英语流利说的架构一直在迭代调整。2015年中旬启动了一次较大规模的重构,经历了简单的半个小时会议,大家一致支持,开启了英语流利说这次的架构演进之路。
这是当时的Task,也是今天我们入手介绍的主要内容:
I. 英语流利说早期架构
应该有很多小型项目,在快速迭代中也存在着这样的架构,如果你们正想往中大型项目看齐,那么可能这篇文章会是你有效的解决方案之一。
英语流利说Android端早期的架构,主要以内部广播方式进行必要的解耦,随着不断的迭代,虽然基本的核心公用代码根据特性已经衍生出了A、B、C Module,但是上层业务复杂度不断增加,各模块相互耦合越发严重,虽然一直都有在架构上做一些小的调整,但都无法根治问题,以此维护性的问题便逐渐凸现。
II. 英语流利说核心架构
这套架构的核心思想
Plugin模式
是借鉴了国内公认最优秀的Android项目所采用的架构,在保留架构核心思想的基础上,以尽量轻,尽量简单的原则做了一些减法以及调整。除了核心架构,我们也做了很多辅助架构为了支撑整套架构灵活性、轻便性。
如上图,整个项目清晰的被拆分为三个层级: 基础层、功能模块层、App模块(Application层),其中功能模块层中的各个功能模块是我们需要解耦出来的,而基础层的每个模块遵循单向依赖关系: 从距离功能模块层最近的中央控制的center
模块、再往下的负责全局监控的monitor
模块、公共布局相关的ui
模块、公共网络数据相关的net
模块、公共底层工具的sdk
模块直到最基本的为国际化做准备的language
模块、供引入第三方库并二次封装的support
模块。
1. 核心架构说明
整体特性
这套架构最明显的特征就是对功能模块层中的每个模块进行了解耦,如下图,使得App模块可以轻易的取消对任何功能模块的依赖而不影响编译与使用,因此我们也将其称为
Plugin模式
。
这套架构是就对各功能模块解耦展开的,而解耦就如A与B需要解耦,引入C,让A、B都依赖C。关系如上图,我们需要对A、B模块解耦,让A、B模块都依赖中央控制center
模块(下文简称中控模块),并且在中控模块中定义A、B模块需要对外开放的接口,在A、B模块中实现各自的接口,然后在App模块中通过反射将A、B模块中的实现传入中控模块,这样App模块、A模块、B模块都可以通过中控进行对各个功能模块进行访问,而当App模块没有依赖A模块时,中控模块会返回在中控模块实现的一个EmptyAPlugin
,至此完成整个环路。
2. 引入多进程层
在基础层中嵌入多进程层,主要是由于在Android中内存共享每个JVM是独立的,在架构层面让所有的各自非UI进程的数据结构都是
Package Visible
,防止被非当前进程调用。
模块命名前缀为
lls_process
是进程模块,并且每个模块的区分以进程为单位
1. 多进程的原因
其实在后来的演进中,我们为了减少因为进程调度对手机资源(CPU、I/O)的消耗,尽可能的合并以及缩减了各类进程(保持一个常驻进程、多个以生命周期为界限的短生命周期非常驻进程)。
多进程化当时有受到了业界某大型安全应用在InfoQ上的一个关于大型移动应用开发的演讲的启发,他们谈到了在一些特定场景下的优势,以及他们从之前的6个进程演变为17个进程,从而使得应用变得更加的稳定。
与其说原因,不如说是谈谈适用的场景:
提高UI进程的稳定性以及各进程各自的稳定性。
独立组件充分解耦,充分独立。
为用户节约内存,更加灵活(如: 只保留一个非主进程的来满足聊天的推送)。
减少引入部分第三方组件所带来的风险。
更有效的做UI进程的有损体验(如: 打分进程CRASH以后,在用户使用过程中,通过重启打分进程重新录音打分的机制,尽量减少用户的体验损失)。
由于独立进程在自己的JVM上面,内存方面不会对UI进程的内存分配造成直接的影响,因此在一些内存占用较多如大图预览的时候,可以一次性使用,一次性回收。
2. 多进程通讯架构
这套架构是封装了非UI进程组件用于让非UI进程的Service快速集成并接受绑定Binder与UI进程的UIGuard组件进行IPC,如上图,基本原则就是:
UI进程只可通过UIGuard与另外一个进程的Service进行通信。
Service单向引用其所在进程的业务层,反向的信息流通过EventBus的形式流通。
UIGuard被UI进程的业务层单向引用,反向的信息流也是通过EventBus的形式流通。
Service业务层可通过Binder跨进程通信时对于非
oneway
的接口Block住当前线程等待接口回传的机制,再通过UIGuard转发透传Event从而实现直接向UI进程索要数据。
其实多进程架构我们已经通过我们的开源库lingochamp/FileDownloader对外开源,不过为了FileDownloader独立进程与非独立进程的灵活切换,因此这套架构在FileDownloader上已经迭代为另外的版本,如果感兴趣可以看看早些的commit。
III. 英语流利说常用辅助架构
主要是对核心架构的辅助,以及一些在核心架构体系下遇到一些问题的解决。
1. 异步加载机制
由于核心架构中是通过反射的机制注入每个模块的具体实现,而这块的反射耗时每次都会在百毫秒左右,这是用户每次打开应用或每次UI进程被回收以后恢复都会遇到的耗时问题,因此有了异步加载机制(当然应对类似体验问题,也有一些取巧的方法可以借鉴,比如腾讯新闻的闪屏Activity的Window的背景直接使用了一张闪屏的背景图片)。
我们都知道系统已经有一套通过同步序列化的恢复机制,但是相比而言,在这个场景下我们更需要的是一个异步的机制,也就是下面这套架构所提供的机制。
这套架构简单粗暴,但十分有效: 对Activity
系统维系的生命周期转一层的方式,从架构方面对业务层获取到的Activity生命周期进行控制。
2. 拓展灵活性EventBus
这个主要是为了弥补在一些情景下,核心架构中的接口显得不够灵活,比如有些操作需要在各个功能模块间透传。但是慎用该类方式,因为考虑到可维护性。由于这套架构网络已经很多衍生了,就不耽误各位时间多说了,有点类似简化的本地广播模型。主要作用是将发送端与接收端充分解耦。
3. 监控系统体系架构
对应用的监控是维护应用稳定性与对应用性能量化不可或缺的一个重要的环节,英语流利说在核心架构搭建之初就已经设计了监控模块,主要是做以下监控:
ANR监控
主要通过系统API监控/data/anr/traces.txt
文件的变化,进而对其进行分析。
Crash监控
我们Crash上报部分采用了支持收集native层异常的第三方库: Fabric,在此基础上我们做了以下拓展:
Crash写文件,主要结合命令系统体系使用输出最近都的crash。
非UI进程的Crash不走系统默认Crash处理,走有损体验体系,对于用户不可见。
Activity生命周期监控
主要是基于 Application.ActivityLifecycleCallbacks
,这里的监控主要是辅助以下操作:
结合图片加载监控体系,保证在打开新页面的时候,旧页面的图片加载全部暂停。
一些服务的注销。
Activity
从ContentView
开始遍历扫描,通过置空可能导致泄漏的对象来对Activity
进行空壳化处理。
内存泄漏监控
我们也是使用Leakcanary这个开源库,在Staging环境上进行检测。
一般性业务层级监控:
这里涉及到一个日志选择性上报系统,主要是结合日志系统用于调试难以复现的BUG(默认是关闭的,目前支持用户在应用中主动打开与上传)这套系统受限于篇幅,以后再分享,也许我们会考虑进行开源。
其他监控
如下载监控、DNS劫持监控等。
在现有的核心架构体系下,监控的核心作用点都是其他模块,比如对UI模块的监控,对网络模块的监控等,但是其所在的基础层是一个自上向下的单向依赖关系,因此这里又会涉及到一个辅助组件
MonitorPool
下图是注册一个图片加载监控的案例。
4. 管理员系统体系架构
管理体系主要是为了测试人员以及开发人员在应用测试阶段能够通过一些绿色通道开启一些对外界用户不开放的功能。
这套系统主要是考虑到安全性,因此放到了编译阶段完成。
IV. 英语流利说常用支持型架构
1. 文件存储体系
LLSPath
主要支持版本迭代,根据版本升级提供类似数据库一套的数据迁移策略。LLSUserPath
在LLSPath
的基础上,提供用户切换,相关路径变更以及相关的操作。
2. 防DNS劫持体系
采用HttpDNS,这块我们的核心思想是尽量的精简轻量并尽量维持与现有系统提供的DNS体系相同的策略,主要通过关注以下几点实现:
存储DNS的文件的大小,当超过阀值大小时LRU规则进行维护。
每个Host对应DNS根据不同的TTL进行维护。
当存在备选IP时,当延时最低IP连接失败以后,备选IP替换上去。
基于在请求对应Host的IP的时候,远端已经根据延时排序返回对应的IP队列,本地不再做多余的复杂存储与测试(如测速、稳定性测试存储、复杂的抉择策略等)。
3. 图片加载体系
已经开源,欢迎PR: lingochamp/QiniuImageLoader
在全局图片加载漏斗模型的前提下,拥有以下特点:
全局默认WEBP,支持指定任意格式获取图片。
所有图片操作(包括缩放、高斯模糊、CenterCrop等)都放到云端处理,因此保证客户端尽可能的减少了CPU、网络、I/O资源的消耗,特别在比较差的手机上尤为明显。
所有的图片请求,默认强制需要提供需要的尺寸规格(如,需要一个宽度为100dp的CenterCrop的图片,需要一个最大宽度不超过屏幕宽度一半的等比例缩放的图片)。
接口简单,易用。
4. 下载体系
已经开源,欢迎PR: ingochamp/FileDownloader
我们的下载体系主要拥有以下特点:
高并发、高稳定性。
灵活配置,如配置,下载服务运行在UI进程还是运行在独立进程、配置主动确保flush到本地的间隔等。
接口简单、便于用于简单的场景也便于用于复杂的场景。
便于监控,已有很好的监控接口。
在各类大小架构的支撑下,英语流利说的整体架构目前已经趋于稳定,但是,前方还有很多需要我们去做的,如单元测试在架构层保证规范化与常规化;如策略型需求在架构层保证可配置化;如在架构层面基于Annotation Processing封装实现快速减少重复Coding等等。无论如何,我们始终秉承,在不断发展与演进的过程中,也能不断的回馈社区。无论是源码还是架构思想本身都是在快速的贬值,唯有不断的实践、不断的迭代,不断的发展,才能使得世界更加美好。