关于模块化(组件化)这个问题,我想每个开发者可能都认真的思考过。随着项目的开发,业务不断壮大,业务模块越来越多,各个模块间相互引用,耦合越来越严重,同时有些项目(比如我们公司)还伴随着子应用单独包装推广,影子应用单独发布等等需求,重新调整架构迫在眉睫。今天,我们就来聊聊模块化(组件化),这篇文章同时也是我这几年,对项目架构的理解。
最初的超小型项目
当我们最开始做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调用的具体流程是这样的:
-
任意代码创建一个
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
的地方,会返回Not Found提示,实际开发中可以根据需求做具体的处理。
可测试性增强
由于每个Module
并不依赖其他的Module
,所以在开发过程中,我们只针对自己的模块进行开发,并可以建一个测试App来进行白盒测试。
复用性增强
关于复用性这块。作者所处的行业是招商投资这块,这个行业需要围绕主业务开发很多影子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:
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启动流程如下:
首先,我们先把所有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、提建议。
或者直接在项目中引入:
|