随着业务层的复杂度不断增加,英语流利说Android的代码架构经历了一场华丽的蜕变,本文详细讲解了英语流利说Android端如何从一个中小型项目架构转为中大型项目架构的过程。
本文已经发布到英语流利说技术团队公众号,可请直接访问: 英语流利说Android架构演进 - 流利说技术团队
今天给大家分享的是英语流利说Android端的代码架构的演进,标题挺高大上的,其实也并非多高大上的东西,整个演进过程,也是借鉴了业界很多大型应用在架构上的沉淀以及思想,可能有些东西还有点老生常谈,不过我们保证尽量都是干货。
英语流利说的架构一直在迭代调整。2015年中旬启动了一次较大规模的重构,经历了简单的半个小时会议,大家一致支持,开启了英语流利说这次的架构演进之路✌✌。
这是当时的Task,也是今天我们入手介绍的主要内容:
应该有很多小型项目,在快速迭代中也存在着这样的架构,如果你们正想往中大型项目看齐,那么可能这篇文章会是你有效的解决方案之一。
英语流利说Android端早期的架构,主要以内部广播方式进行必要的解耦,随着不断的迭代,虽然基本的核心公用代码根据特性已经衍生出了A、B、C Module,但是上层业务复杂度不断增加,各模块相互耦合越发严重,虽然一直都有在架构上做一些小的调整,但都无法根治问题,以此维护性的问题便逐渐凸现。
这套架构的核心思想 Plugin模式
是借鉴了 国内公认最优秀的Android项目所采用的架构 ,在保留架构核心思想的基础上,以尽量轻,尽量简单的原则做了一些减法以及调整。除了核心架构,我们也做了很多辅助架构为了支撑整套架构灵活性、轻便性。
如上图,整个项目清晰的被拆分为三个层级: 基础层、功能模块层、App模块(Application层),其中功能模块层中的各个功能模块是我们需要解耦出来的,而基础层的每个模块遵循单向依赖关系: 从距离功能模块层最近的中央控制的 center
模块、再往下的负责全局监控的 monitor
模块、公共布局相关的 ui
模块、公共网络数据相关的 net
模块、公共底层工具的 sdk
模块直到最基本的为国际化做准备的 language
模块、供引入第三方库并二次封装的 support
模块。
这套架构最明显的特征就是对功能模块层中的每个模块进行了解耦,如下图,使得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
,至此完成整个环路。
在基础层中嵌入多进程层,主要是由于在Android中内存共享每个JVM是独立的,在架构层面让所有的各自非UI进程的数据结构都是 Package Visible
,防止被非当前进程调用。
模块命名前缀为 lls_process
是进程模块,并且每个模块的区分以进程为单位
其实在后来的演进中,我们为了减少因为进程调度对手机资源(CPU、I/O)的消耗,尽可能的合并以及缩减了各类进程(保持一个常驻进程、多个以生命周期为界限的短生命周期非常驻进程)。
多进程化当时有受到了 业界某大型安全应用在InfoQ上的一个关于大型移动应用开发的演讲 的启发,他们谈到了在一些特定场景下的优势,以及他们从之前的6个进程演变为17个进程,从而使得应用变得更加的稳定。
与其说原因,不如说是谈谈适用的场景:
这套架构是封装了非UI进程组件用于让非UI进程的Service快速集成并接受绑定Binder与UI进程的UIGuard组件进行IPC,如上图,基本原则就是:
oneway
的接口Block住当前线程等待接口回传的机制,再通过UIGuard转发透传Event从而实现直接向UI进程索要数据。 其实多进程架构我们已经通过我们的开源库 lingochamp/FileDownloader 对外开源,不过为了FileDownloader独立进程与非独立进程的灵活切换,因此这套架构在FileDownloader上已经迭代为另外的版本,如果感兴趣可以看看早些的commit。
主要是对核心架构的辅助,以及一些在核心架构体系下遇到一些问题的解决。
由于核心架构中是通过反射的机制注入每个模块的具体实现,而这块的反射耗时每次都会在百毫秒左右,这是用户每次打开应用或每次UI进程被回收以后恢复都会遇到的耗时问题,因此有了异步加载机制(当然应对类似体验问题,也有一些取巧的方法可以借鉴,比如腾讯新闻的闪屏Activity的Window的背景直接使用了一张闪屏的背景图片)。
我们都知道系统已经有一套通过同步序列化的恢复机制,但是相比而言,在这个场景下我们更需要的是一个异步的机制,也就是下面这套架构所提供的机制。
这套架构简单粗暴,但十分有效: 对 Activity
系统维系的生命周期转一层的方式,从架构方面对业务层获取到的Activity生命周期进行控制。
这个主要是为了弥补在一些情景下,核心架构中的接口显得不够灵活,比如有些操作需要在各个功能模块间透传。但是慎用该类方式,因为考虑到可维护性。由于这套架构网络已经很多衍生了,就不耽误各位时间多说了,有点类似简化的本地广播模型。主要作用是将发送端与接收端充分解耦。
对应用的监控是维护应用稳定性与对应用性能量化不可或缺的一个重要的环节,英语流利说在核心架构搭建之初就已经设计了监控模块,主要是做以下监控:
主要通过系统API监控 /data/anr/traces.txt
文件的变化,进而对其进行分析。
我们Crash上报部分采用了支持收集native层异常的第三方库: Fabric,在此基础上我们做了以下拓展:
主要是基于 Application.ActivityLifecycleCallbacks
,这里的监控主要是辅助以下操作:
Activity
从 ContentView
开始遍历扫描,通过置空可能导致泄漏的对象来对 Activity
进行空壳化处理。 我们也是使用Leakcanary这个开源库,在Staging环境上进行检测。
这里涉及到一个日志选择性上报系统,主要是结合日志系统用于调试难以复现的BUG(默认是关闭的,目前支持用户在应用中主动打开与上传)这套系统受限于篇幅,以后再分享,也许我们会考虑进行开源。
如下载监控、DNS劫持监控等。
在现有的核心架构体系下,监控的核心作用点都是其他模块,比如对UI模块的监控,对网络模块的监控等,但是其所在的基础层是一个自上向下的单向依赖关系,因此这里又会涉及到一个辅助组件 MonitorPool
下图是注册一个图片加载监控的案例。
管理体系主要是为了测试人员以及开发人员在应用测试阶段能够通过一些绿色通道开启一些对外界用户不开放的功能。
这套系统主要是考虑到安全性,因此放到了编译阶段完成。
LLSPath
主要支持版本迭代,根据版本升级提供类似数据库一套的数据迁移策略。 LLSUserPath
在 LLSPath
的基础上,提供用户切换,相关路径变更以及相关的操作。
采用HttpDNS,这块我们的核心思想是尽量的精简轻量并尽量维持与现有系统提供的DNS体系相同的策略,主要通过关注以下几点实现:
已经开源,欢迎PR: lingochamp/QiniuImageLoader
在全局图片加载漏斗模型的前提下,拥有以下特点:
已经开源,欢迎PR: ingochamp/FileDownloader
我们的下载体系主要拥有以下特点:
在各类大小架构的支撑下,英语流利说的整体架构目前已经趋于稳定,但是,前方还有很多需要我们去做的,如单元测试在架构层保证规范化与常规化;如策略型需求在架构层保证可配置化;如在架构层面基于Annotation Processing封装实现快速减少重复Coding等等。无论如何,我们始终秉承,在不断发展与演进的过程中,也能不断的回馈社区。无论是源码还是架构思想本身都是在快速的贬值,唯有不断的实践、不断的迭代,不断的发展,才能使得世界更加美好。
© 2016, 流利说技术团队( https://www.liulishuo.com/ ). Licensed under the Creative Commons Attribution-NonCommercial 3.0 license (This license lets others remix, tweak, and build upon a work non-commercially, and although their new works must also acknowledge the original author and be non-commercial, they don’t have to license their derivative works on the same terms). http://creativecommons.org/licenses/by-nc/3.0/