您的位置:首页 > 运维架构 > 网站架构

Android架构思考(模块化、多进程)

2017-02-14 16:35 253 查看
关于模块化(组件化)这个问题,我想每个开发者可能都认真的思考过。随着项目的开发,业务不断壮大,业务模块越来越多,各个模块间相互引用,耦合越来越严重,同时有些项目(比如我们公司)还伴随着子应用单独包装推广,影子应用单独发布等等需求,重新调整架构迫在眉睫。今天,我们就来聊聊模块化(组件化),这篇文章同时也是我这几年,对项目架构的理解。


最初的超小型项目

当我们最开始做Android项目的时候,大多数人都是没考虑项目架构的,我们先上一张图。


2012年开发的一个小项目

这个分包结构有没有很熟悉,各种组件都码在一个包里,完全没有层级结构,业务、界面、

逻辑都耦合在一起。这是我12年底刚开始入门Android的时候开发的一个小项目,半年后,来了个小伙伴,然后我们一起开发,然后天天因为谁修改了谁的代码打的不可开交。


架构改进,小型项目

再后来开发App,人员比之前多了,所以不能按照以前那样了,必须得重构。于是我把公用的代码提取出来制作成SDK基础库,把单独的功能封装成Library包,不同业务通过分包结构分到不同module下,组内每人开发自己的module。刚开始都还轻松加愉快,并行开发啥的,一片融洽的场景,如下图。


刚刚重构之后的架构

随着时间推移,我们的App迭代了几个版本,这几个版本也没什么别的,大体来讲就是三件事情:

扩展了一些新业务模块,同时模块间相互调用也增加了。

修改增加了一些新的库文件,来支持新的业务模块。

对Common SDK进行了扩展、修复。

很惭愧,就做了一些微小的工作,但是架构就变成下图这样。


做了几件微小的工作之后

可以看到,随着几个版本业务的增加,各个业务某块之间耦合愈发严重,导致代码很难维护,更新,更别说写测试代码了。虽然后期引入统一广播系统,一定程度改善了模块间相互引用的问题,但是局限性和耦合性还是很高,没办法根治这个问题。这个架构做到最后,扩展性和可维护性都是很差,并且难以测试,所以最终被历史的进程所抛弃。


中小型项目,路由架构

时间很快就来到了2015年,这一年动态加载、热修复很火,360、阿里等大公司先后开源了自己的解决方案,如droidplugin、andfix等。在研究了一圈发现,这些技术对架构升级有一定的帮助,尤其是droidplugin的加载apk的思想,能很好地解决耦合度高、方法数超过65535、动态修复bug等问题,不过由于项目本身不是很大,并且没有专门的人来维护架构,所以最后放弃了功能强大、但是问题也同样多的插件化,退而求其次,选择了利用路由机制来实现组件化解耦。

关于路由机制,熟悉iOS开发的朋友可能并不陌生,在iOS上有很多架构方案都是采用路由机制来时间模块之间的解耦的,比如VIPER(View Interactor Presenter Entity Routing)思想等等。其实思路都是相同的,Android上面组件化也是通过公用的路由,来实现模块与模块之间的隔离。


实现原理

我们先来看下路由架构图。


路由架构

大图点我

通过上图可以看到,我们在最基础的Common库中,创建了一个路由
Router
,中间有n个模块
Module
,这个
Module
实际上就是Android
Studio中的module,这些
Module
都是Android Library
Module,最上面的Module Main是可运行的Android Application Module。

这几个
Module
都引用了Common库,同时Main Module还引用了A、B、N这几个
Module
,经过这样的处理之后,所有的
Module
之间的相互调用就都消失了,耦合性降低,所有的通信统一都交给Router来处理分发,而注册工作则交由Main
Module去进行初始化。这个架构思想其实和Binder的思想很类似,采用C/S模式,模块之间隔离,数据通过共享区域进行传递。模块与模块之间只暴露对外开放的Action,所以也具备面向接口编程思想。

图中的红色矩形代表的是行动
Action
Action
是具体的执行类,其内部的invoke方法是具体执行的代码逻辑。如果涉及到并发操作的话,可以在invoke方法内加入锁,或者直接在invoke方法上加上synchronized描述。

图中的黄色矩形代表的是供应商
Provider
,每个
Provider
中包含1个或多个
Action
,其内部的数据结构以HashMap来存储Action。首先HashMap查询的时间复杂度是O(1),符合我们对调用速度上的要求,其次,由于我们是统一进行注册,所以在写入时并不存在并发线程并发问题,在读取时,并发问题则交由Action的invoke去具体处理。在每一个
Module
内都会有1个或多个供应商
Provider
(如果不包含
Provider
,那么这个
Module
将无法为其他
Module
提供服务)。

途中蓝色巨星代表的是路由
Router
,每个
Router
中包含多个
Provider
,其内部的数据结构也是以HashMap来存储
Provider
,原理也和
Provider
是一样的。之所以用了两次HashMap,有两点原因,一个是因为这样做,不容易导致
Action
的重名,另一个是因为在注册的时候,只注册
Provider
会减少注册代码,更易读。并且由于HashMap的查询时间复杂度是O(1),所以两次查找不会浪费太多时间。当查找不到对应
Action
的时候,Router会生成一个
ErrorAction
,会告之调用者没有找到对应的
Action
,由调用者来决定接下来如何处理。


一次请求流程

通过Router调用的具体流程是这样的:


Router时序图

任意代码创建一个
RouterRequest
,包含
Provider
Action
信息,向
Router
进行请求。

Router
接到请求,通过
RouterRequest
Provider
信息,在内部的HashMap中查找对应的
Provider


Provider
接到请求,在内部的HashMap中查找到对应的
Action
信息。

Action
调用invoke方法。

返回invoke方法生成的
ActionResult


Result
封装成
RouterResponse
,返回给调用者。


耦合降低

所有的
Module
之间的相互依赖没有了,我们可以在主app中,取消任意的
Module
引用而不影响整体App的编译及运行。


取消对Module N的依赖

如图所示,我们取消了对
Module N
的依赖,整体应用依然可以稳定运行,遇到调用
Module
N
的地方,会返回Not Found提示,实际开发中可以根据需求做具体的处理。


可测试性增强

由于每个
Module
并不依赖其他的
Module
,所以在开发过程中,我们只针对自己的模块进行开发,并可以建一个测试App来进行白盒测试。


测试Module
A


复用性增强

关于复用性这块。作者所处的行业是招商投资这块,这个行业需要围绕主业务开发很多影子APP,将覆盖面扩大(有点类似58->58租房、58招聘,美团->美团外卖等)。这个时候,这个架构的复用性就体现出来了,我们可以把业务进行拆分,然后写一个包装App,就可以生成一个独立的影子APP,这个影子APP用到哪些
Module
就引用哪些就可以了,开发迅速,并且后期
Module
业务有变化,也不用更改所有的代码,减少了代码的复制。比如我们就曾经把IM模块和投资咨询模块单独拿出来,写了一些界面和样式,就生成了“招商经纪人”App。


支持并行开发

整套架构符合Git的Branch思想,每个
Module
有专门的Branch进行开发,在进行集成测试的时候再去与主分支进行合并。每个
Module
都有自己单独的版本控制,便于管理及问题溯源。


多进程思考,中型项目

随着项目的不断扩大,App在运行时的内存消耗也在不断增加,而且有时线上的BUG也会导致整体崩溃。为了保证良好的用户体验,减少对系统资源的消耗,我们开始考虑采取多进程重新架构程序,通过按需加载,及时释放,达到优化的目的。


多进程优势

多进程的优点和使用场景,之前在《Android多进程使用场景》中也做过介绍,大体优点有这么几个:

提高各个进程的稳定性,单一进程崩溃后不影响整个程序。

对于内存的时候更可控,可以通过手工释放进程,达到内存优化目的。

基于独立的JVM,各个模块可以充分解耦。

只保留daemon进程的情况下,会使应用存活时间更长,不容易被回收掉。


潜在问题

但是启用多线程,那就意味着Router系统的失效。Router是JVM级别的单例模式,并不支持跨进程访问。也就是说,你的后台进程的所有
Provider
Action
,是注册给后台Router的。当你在前台进程调用的时候,根本调用不到其他进程的
Action


解决方案

其实解决的方法也并不复杂。原来的路由系统还可以继续使用,我们可以把整套架构想象成互联网,现在多个进程有多个路由,我们只需要把多个路由连接到一起,那么整个路由系统还是可以正常运行的。所以我们把原有的路由
Router
称之为本地路由
LocalRouter
,现在,我们需要提供一个IPS、DNS供应商,那就创建一个进程,该进程的作用就是注册路由,链接路由,转发报文,我们称之为广域路由
WideRouter


我们先来看下路由连接架构图


路由连接架构

点击大图

如图所示,竖直方向上,每一列,代表一个进程,通过虚线隔开,分别有Process WideRouter、Process Main、Process A、···、Process N这些进程。浅黄色的代表
WideRouter
,深黄色的代表
WideRouter
的守护Service。浅蓝色的代表每个进程的
LocalRouter
,深蓝色的代表每个
LocalRouter
的守护Service。
WideRouter
通过AIDL与每个进程
LocalRouter
的守护Service绑定到一起,每个
LocalRouter
也是通过AIDL与
WideRouter
的守护Service绑定到一起,这样,就达到了所有路由都是双向互连的目的。


事件分发

之前单一路由的事件分发是通过两层HashMap查找
Provider
Action
,进行事件下发。那么现在在外面加了一层
WideRouter
,那么我们再加一层
Domain
Domain
对应的是Android应用内,各个进程的进程名。通常情况下,如果事件是在同一进程下,那么就类似于局域网内部事件传递,不需要通过
WideRouter
,直接内部按照之前的路由逻辑进行转发,如果不在相同进程内,就由
WideRouter
进行进程间通信,达到跨进程调用的效果。

事件请求
RouterRequest
可以写成两种,一种是URL,一种JSON。(内部处理的时候统一使用JSON),同时也提供了对URL和JSON的解析方法,方便使用。

URL:xxxDomain/xxxProvider/xxxAction?data1=xxx&data2=xxx

这就和Http请求很像了。这样做的好处就是对后续WebView上可以非常便利得直接调用本地
Action


JSON:

{
   domain: xxx,
   provider: xxx,
   action: xxx,
   data{
       data1: xxx,
       data2: xxx
   }
}

JSON方式简单明了,可作为接口返回值由服务器下发给客户端。

下面仔细讲一下一次跨进程请求,事件是如何传递的:


事件传递图

点击大图

从图中可以清晰地看出,我们主要是分两大部分去完成事件分发传递的。

第一部分,跨进程判断目标
Action
是否是异步程序。

第二部分,跨进程执行目标
Action
调用。

首先我们先通过
Domain
Provider
Action
去跨进程查找是否是异步程序。如果是异步程序,那么我们直接生成RouterResponse(Step13),并且,将Step14-Step24统一封装成Future,放在RouterResponse中,直接返回。如果是同步程序,那么就在当前方法内执行Step14-Step24,将返回结果放入RouterResponse内(Step25),直接返回。这么做的目的是,我们的路由调用方法
route(RouterRequest)
默认是同步方法,不耗时的,可以直接在主线程里调用而不造成阻塞,不造成ANR。如果调用的目标
Action
是异步的,那么可以利用Java的FutureTask原理,调用
RouterResponse
get()
方法,获取结果。这个
get()
方法有可能是耗时的,是否耗时,取决于
RouterResponse.isAsync
的值是否是
true


至于本地事件分发,还是与之前的Router模式,从Step17到Step21,都是我们上文中,单进程同步Router分发机制,没有作任何改变。


多进程Application逻辑分发

在多进程中,每启动一个新的进程,都会重新创建一次Application,所以,我们需要把各个进程的Application逻辑剥离出来,然后根据不同的
Process
Name
,选择不同的Application逻辑进行处理。

实际的Application启动流程如下:


多进程Application启动流程

首先,我们先把所有
ApplicationLogic
注册到Application中,然后,Application会根据注册时的进程名信息进行筛选,选择相同进程名的
ApplicationLogic
,保存到本进程中,然后,对这些本进程的
ApplicationLogic
进行实例化,最后,调用
ApplicationLogic
onCreate
方法,实现
ApplicationLogic
Application
生命周期同步,同时还有
onTerminate
onLowMemory
onTrimMemory
onConfigurationChanged
等方法,与
onCreate
一致。


结束进程,释放内存

在我们不使用某些进程的时候,比如听音乐的时候,可以把主界面关掉等等。我们可以调用对应进程的
LocalRouter
stopSelf()
方法,该方法可以使本进程与
WideRouter
进行解绑,然后我们在手动关掉进程内的其他组件,最后调用
System.exit()
,达到释放内存的目的。合理的释放内存,能有效的改善用户体验。


小结

这篇文章大概讲了一下作者这几年对Android架构的理解。其实本文中没有什么很深的技术点,大多是一些设计模式,架构思想。这套框比起大公司的一些优秀的动态更新、编译分包、apk插件化加载,还是简单很多的,更适合中小型应用。

这套框架目前还有比较多可以改进的地方,目前正在整理的:

增加对
Action
的动态关闭功能。

通过
Instant Run
原理,实现
Action
的热更新。

增加
Message Pool
,实现
Request
Response
的循环利用,减少GC触发。

已解决《高并发对象池思考》

优化
Message
在传递过程中的打包,拆包的速度,提升整体性能。

etc.

本文项目地址:ModularizationArchitecture,欢迎大家star、fork、提建议。

或者直接在项目中引入:

compile 'com.spinytech.ma:macore:0.1.2'


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息