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

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

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

最初的超小型项目

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

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

业务、界面、

逻辑都耦合在一起

。这是我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,所以也具备 面向接口编程思想

图中的红色矩形代表的是行动 ActionAction 是具体的执行类,其内部的 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调用的具体流程是这样的:

  1. 任意代码创建一个 RouterRequest ,包含 ProviderAction 信息,向 Router 进行请求。
  2. Router 接到请求,通过 RouterRequestProvider 信息,在内部的HashMap中查找对应的 Provider
  3. Provider 接到请求,在内部的HashMap中查找到对应的 Action 信息。
  4. Action 调用invoke方法。
  5. 返回invoke方法生成的 ActionResult
  6. 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思想,基于主线,分支单独开发,最后再回归主线这种思路。这里只是思路和branch相似,实际的开发过程中,我们每个module可以是一个branch,也可以是一个仓库。每个模块都需要自己有单独的版本控制,便于问题管理及溯源。主项目对各个模块的引用可以是直接引用,也可以是导出aar引用,或者是上传JCenter Maven等等方式。不过思路是统一的:继承公共->独立开发->主进程合并。

多进程思考,中型项目

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

多进程优势

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

  • 提高各个进程的稳定性,单一进程崩溃后不影响整个程序。
  • 对于内存的时候更可控,可以通过手工释放进程,达到内存优化目的。
  • 基于独立的JVM,各个模块可以充分解耦。
  • 只保留daemon进程的情况下,会使应用存活时间更长,不容易被回收掉。

潜在问题

但是启用多进程,那就意味着Router系统的失效。 Router是JVM级别的单例模式,并不支持跨进程访问 。也就是说,你的后台进程的所有 ProviderAction ,是注册给后台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查找 ProviderAction ,进行事件下发。那么现在在外面加了一层 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 调用。

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

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

多进程Application逻辑分发

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

实际的Application启动流程如下:

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

结束进程,释放内存

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

小结

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

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

  • 增加对 Action 的动态关闭功能。
  • 通过 Instant Run 原理,实现 Action 的热更新。
  • 增加 Message Pool ,实现 RequestResponse 的循环利用,减少GC触发。
    已解决《高并发对象池思考》
  • 优化 Message 在传递过程中的打包,拆包的速度,提升整体性能。
  • etc.

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

或者直接在项目中引入:

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

相关教程: ModularizationArchitecture 使用教程

如果有疑问或者建议,欢迎联系我,邮箱是: spiny.tech@gmail.com

谢谢大家阅读

原创文章,转载请先联系作者: spiny.tech@gmail.com

我来评几句
登录后评论

已发表评论数()