

万维网发明人 Tim Berners-Lee 谈到设计原理时说过:“简单性和板块化是软件工程的基石;分布式和容错性是互联网的生命。” 由此可见板块化之于软件工程领域的重要性。
从 2016 年开始,板块化在 Android 社区越来越多的被提及。随着移动平台的不断发展,移动平台上的软件慢慢走向复杂化,体积也变得臃肿庞大;为了降低大型软件复杂性和耦合度,同时也为了适应板块重用、多团队并行开发测试等等需求,板块化在 Android 平台上变得势在必行。阿里 Android 团队在年初开源了他们的容器化框架 Atlas 就很大程度说明了当前 Android 平台开发大型商业项目所面临的问题。
那么什么是板块化呢?《 Java 应用架构设计:板块化模式与 OSGi 》一书中对它的定义是:板块化是一种解决复杂系统分解为更好的可管理板块的方式。
上面这种形容太过生涩难懂,不够直观。下面这种类比的方式则可能加容易了解。
我们可以把软件看做是一辆汽车,开发一款软件的过程就是生产一辆汽车的过程。一辆汽车由车架、发动机、变数箱、车轮等一系列板块组成;同样,一款大型商业软件也是由各个不同的板块组成的。
汽车的这些板块是由不同的工厂生产的,一辆 BMW 的发动机可能是由位于德国的工厂生产的,它的自动变数箱可能是 Jatco(世界三大变速箱厂商之一)位于日本的工厂生产的,车轮可能是中国的工厂生产的,最后交给华晨宝马的工厂统一组装成一辆完整的汽车。这就相似于我们在软件工程领域里说的多团队并行开发,最后将各个团队开发的板块统一打包成我们可使用的 App 。
一款发动机、一款变数箱都不可能只应用于一个车型,比方同一款 Jatco 的 6AT 自动变速箱既可能被安装在 BMW 的车型上,也可能被安装在 Mazda 的车型上。这就好像软件开发领域里的板块重用。
到了冬天,特别是在北方我们可能需要开着车走雪路,为了安全起见往往我们会将汽车的公路胎更新为雪地胎;轮胎可以很轻易的更换,这就是我们在软件开发领域谈到的低耦合。一个板块的更新替换不会影响到其它板块,也不会受其它板块的限制;同时这也相似于我们在软件开发领域提到的可插拔。
上面的类比很清晰的说明的板块化带来的好处:
在《 Android 项目架构演进》这篇文章中,我详情了安居客 Android 端的板块化设计方案,这里我还是拿它来举例。但首先要对本文中的组件和板块做个区别定义
组件:指的是单一的功能组件,如地图组件(MapSDK)、支付组件(AnjukePay)、路由组件(Router)等等;
板块:指的是独立的业务板块,如新房板块(NewHouseModule)、二手房板块(SecondHouseModule)、即时通讯板块(InstantMessagingModule)等等;板块相对于组件来说粒度更大。
具体设计方案如下图:

整个项目分为三层,从下至上分别是:
我们在谈板块化的时候,其实就是将业务板块层的各个功能业务拆分层独立的业务板块。所以我们进行板块化的第一步就是业务板块划分,但是板块划分并没有一个业界通用的标准,因而划分的粒度需要根据项目情况进行正当把控,这就需要对业务和项目有较为透彻的了解。拿安居客来举例,我们会将项目划分为新房板块、二手房板块、IM 板块等等。
每个业务板块在 Android Studio 中的都是一个 Module ,因而在命名方面我们要求每个业务板块都以 Module 为后缀。如下图所示:

对于板块化项目,每个单独的 Business Module 都可以单独编译成 APK。在开发阶段需要单独打包编译,项目发布的时候又需要它作为项目的一个 Module 来整体编译打包。简单的说就是开发时是 Application,发布时是 Library。因而需要在 Business Module 的 build.gradle 中加入如下代码:
if(isBuildModule.toBoolean()){ apply plugin: 'com.android.application'}else{ apply plugin: 'com.android.library'}isBuildModule 在项目根目录的 gradle.properties 中定义:
isBuildModule=false
同样 Manifest.xml 也需要有两套:
sourceSets { main { if (isBuildModule.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/release/AndroidManifest.xml' } }}如图:

debug 模式下的 AndroidManifest.xml :
<application ... > <activity android:name="com.baronzhang.android.newhouse.NewHouseMainActivity" android:label="@string/new_house_label_home_page"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity></application>realease 模式下的 AndroidManifest.xml :
<application ... > <activity android:name="com.baronzhang.android.newhouse.NewHouseMainActivity" android:label="@string/new_house_label_home_page"> <intent-filter> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <action android:name="android.intent.action.VIEW" /> <data android:host="com.baronzhang.android.newhouse" android:scheme="router" /> </intent-filter> </activity></application>同时针对板块化我们也定义了少量自己的游戏规则:
对业务进行板块化拆分后,为了使各业务板块间解耦,因而各个 Bussiness Module 都是独立的板块,它们之间是没有依赖关系。那么各个板块间的跳转通讯如何实现呢?
比方业务上要求从新房的列表页跳转到二手房的列表页,那么因为是 NewHouseModule 和 SecondHouseModule 之间并不相互依赖,我们通过想如下这种显式跳转的方式来实现 Activity 跳转显然是不可能的实现的。
Intent intent = new Intent(NewHouseListActivity.this, SecondHouseListActivity.class);startActivity(intent);有的同学可能会想到用隐式跳转,通过 Intent 匹配规则来实现:
Intent intent = new Intent(Intent.ACTION_VIEW, "<scheme>://<host>:<port>/<path>");startActivity(intent);但是这种代码写起来比较繁琐,且容易出错,出错也不太容易定位问题。因而一个简单易用、解放开发的路由框架是必需的了。

?
我自己实现的路由框架分为路由(Router)和参数注入器(Injector)两部分:

Router 提供 Activity 跳转传参的功能;Injector 提供参数注入功能,通过编译时生成代码的方式在 Activity 获取获取传递过来的参数,简化开发。
路由(Router)部分通过 Java 注解结合动态代理商来实现,这一点和 Retrofit 的实现原理是一样的。
首先需要定义我们自己的注解(篇幅有限,这里只列出少部分源码)。
用于定义跳转 URI 的注解 FullUri:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface FullUri { String value();}用于定义跳转传参的 UriParam( UriParam 注解的参数用于拼接到 URI 后面):
@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface UriParam { String value();}用于定义跳转传参的 IntentExtrasParam( IntentExtrasParam 注解的参数最终通过 Intent 来传递):
@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface IntentExtrasParam { String value();}而后实现 Router ,内部通过动态代理商的方式来实现 Activity 跳转:
public final class Router { ... public <T> T create(final Class<T> service) { return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { FullUri fullUri = method.getAnnotation(FullUri.class); StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append(fullUri.value()); //获取注解参数 Annotation[][] parameterAnnotations = method.getParameterAnnotations(); HashMap<String, Object> serializedParams = new HashMap<>(); //拼接跳转 URI int position = 0; for (int i = 0; i < parameterAnnotations.length; i++) { Annotation[] annotations = parameterAnnotations[i]; if (annotations == null || annotations.length == 0) break; Annotation annotation = annotations[0]; if (annotation instanceof UriParam) { //拼接 URI 后的参数 ... } else if (annotation instanceof IntentExtrasParam) { //Intent 传参解决 ... } } //执行Activity跳转操作 performJump(urlBuilder.toString(), serializedParams); return null; } }); } ...}上面是 Router 实现的部分代码,在使用 Router 来跳转的时候,首先需要定义一个 Interface(相似于 Retrofit 的使用方式):
public interface RouterService { @FullUri("router://com.baronzhang.android.router.FourthActivity") void startUserActivity(@UriParam("cityName") String cityName, @IntentExtrasParam("user") User user);}接下来我们即可以通过如下方式实现 Activity 的跳转传参了:
RouterService routerService = new Router(this).create(RouterService.class); User user = new User("张三", 17, 165, 88); routerService.startUserActivity("上海", user);通过 Router 跳转到目标 Activity 后,我们需要在目标 Activity 中获取通过 Intent 传过来的参数:
getIntent().getIntExtra("intParam", 0);getIntent().getData().getQueryParameter("preActivity");为了简化这部分工作,路由框架 Router 中提供了 Injector 板块在编译时生成上述代码。参数注入器(Injector)部分通过 Java 编译时注解来实现,实现思路和 ButterKnife 这类编译时注解框架相似。
首先定义我们的参数注解 InjectUriParam :
@Target(ElementType.FIELD)@Retention(RetentionPolicy.CLASS)public @interface InjectUriParam { String value() default "";}而后实现一个注解解决器 InjectProcessor ,在编译阶段生成获取参数的代码:
@AutoService(Processor.class)public class InjectProcessor extends AbstractProcessor { ... @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //解析注解 Map<TypeElement, TargetClass> targetClassMap = findAndParseTargets(roundEnvironment); //解析完成后,生成的代码的结构已经有了,它们存在InjectingClass中 for (Map.Entry<TypeElement, TargetClass> entry : targetClassMap.entrySet()) { ... } return false; } ...}使用方式相似于 ButterKnife ,在 Activity 中我们使用 Inject 来注解一个全局变量:
@Inject User user;而后 onCreate 方法中需要调用 inject(Activity activity) 方法实现注入:
RouterInjector.inject(this);这样我们即可以获取到前面通过 Router 跳转的传参了。
因为篇幅限制,加上为了便于了解,这里只贴出了极少部分 [Router] 框架的源码。希望进一步理解 Router 实现原理的可以到 GiuHub 去翻阅源码,[Router]的实现还比较简陋,后面会进一步完善功能和文档,之后也会有单独的文章详细详情。
对于多个 Bussines Module 中资源名冲突的问题,可以通过在 build.gradle 定义前缀的方式处理:
defaultConfig { ... resourcePrefix "new_house_" ...}而对于 Module 中有些资源不想被外部访问的,我们可以创立 res/values/public.xml,增加到 public.xml 中的 resource 则可被外部访问,未增加的则视为私有:
<resources> <public name="new_house_settings" type="string"/></resources>板块化的过程中我们常常会遇到重复依赖的问题,假如是通过 aar 依赖, gradle 会自动帮我们找出新版本,而抛弃老版本的重复依赖。假如是以 project 的方式依赖,则在打包的时候会出现重复类。对于这种情况我们可以在 build.gradle 中将 compile 改为 provided,只在最终的项目中 compile 对应的 library ;
其实从前面的安居客板块化设计图上能看出来,我们的设计方案能肯定程度上规避重复依赖的问题。比方我们所有的第三方库的依赖都会放到 OpenSoureLibraries 中,其余需要用到相关类库的项目,只要要依赖 OpenSoureLibraries 就好了。
对于大型的商业项目,在重构过程中可能会遇到业务耦合严重,难以拆分的问题。我们需要先理清业务,再动手拆分业务板块。比方可以先在原价的项目中根据业务分包,在肯定程度上将各业务解耦后拆分到不同的 package 中。比方之前新房和二手房因为同属于 app module,因而他们之前是通过隐式的 intent 跳转的,现在可以先将他们改为通过 Router 来实现跳转。又比方新房和二手房中公用的板块可以先下放到 Business Component Layer 或者者 Basic Component Layer 中。在这一系列工作完成后再将各个业务拆分成多个 module 。
板块化重构需要渐进式的开展,不可一触而就,不要想着将整个项目推翻重写。线上成熟稳固的业务代码,是经过了时间和大量客户考验的;一律推翻重写往往费时费力,实际的效果通常也很不理想,各种问题层出不穷得不偿失。对于这种项目的板块化重构,我们需要一点点的改进重构,可以分散到每次的业务迭代中去,逐渐淘汰掉陈旧的代码。
各业务板块间一定会有公用的部分,按照我前面的设计图,公用的部分我们会根据业务相关性下放到业务组件层(Business Component Layer)或者者基础组件层(Common Component Layer)。对于太小的公有板块不足以构成单独组件或者者板块的,我们先放到相似于 CommonBusiness 的组件中,在后期不断的重构迭代中视情况进行进一步的拆分。过程中完美主义可以有,切记不可过度。
以上就是我在板块化探究实践方面的少量经验,不住之处还望大家指出。
想学习更多Android知识,请加入Android技术开发交流 7520 16839
进群与大牛们一起探讨,还可获取Android高级架构资料、源码、笔记、视频
包括 高级UI、Gradle、RxJava、小程序、Hybrid、移动架构、React Native、性能优化等全面的Android高级实践技术讲解性能优化架构思维导图,和BATJ面试题及答案!
群里免费分享给有需要的朋友,希望能够帮助少量在这个行业发展迷茫的,或者者想系统深入提升以及困于瓶颈的
朋友,在网上博客论坛等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我在这免费分享少量架构资料及给大家。希望在这些资料中都有你需要的内容。
