本文首发于《程序员》杂志五月刊,此版本有部分纠错与调整java
万维网发明人 Tim Berners-Lee 谈到设计原理时说过:“简单性和模块化是软件工程的基石;分布式和容错性是互联网的生命。” 因而可知模块化之于软件工程领域的重要性。android
从 2016 年开始,模块化在 Android 社区愈来愈多的被说起。随着移动平台的不断发展,移动平台上的软件慢慢走向复杂化,体积也变得臃肿庞大;为了下降大型软件复杂性和耦合度,同时也为了适应模块重用、多团队并行开发测试等等需求,模块化在 Android 平台上变得势在必行。阿里 Android 团队在年初开源了他们的容器化框架 Atlas 就很大程度说明了当前 Android 平台开发大型商业项目所面临的问题。git
那么什么是模块化呢?《 Java 应用架构设计:模块化模式与 OSGi 》一书中对它的定义是:模块化是一种处理复杂系统分解为更好的可管理模块的方式。程序员
上面这种描述太过生涩难懂,不够直观。下面这种类比的方式则可能加容易理解。github
咱们能够把软件看作是一辆汽车,开发一款软件的过程就是生产一辆汽车的过程。一辆汽车由车架、发动机、变数箱、车轮等一系列模块组成;一样,一款大型商业软件也是由各个不一样的模块组成的。安全
汽车的这些模块是由不一样的工厂生产的,一辆 BMW 的发动机多是由位于德国的工厂生产的,它的自动变数箱多是 Jatco(世界三大变速箱厂商之一)位于日本的工厂生产的,车轮多是中国的工厂生产的,最后交给华晨宝马的工厂统一组装成一辆完整的汽车。这就相似于咱们在软件工程领域里说的多团队并行开发,最后将各个团队开发的模块统一打包成咱们可以使用的 App 。架构
一款发动机、一款变数箱都不可能只应用于一个车型,好比同一款 Jatco 的 6AT 自动变速箱既可能被安装在 BMW 的车型上,也可能被安装在 Mazda 的车型上。这就如同软件开发领域里的模块重用。app
到了冬天,特别是在北方咱们可能须要开着车走雪路,为了安全起见每每咱们会将汽车的公路胎升级为雪地胎;轮胎能够很轻易的更换,这就是咱们在软件开发领域谈到的低耦合。一个模块的升级替换不会影响到其它模块,也不会受其它模块的限制;同时这也相似于咱们在软件开发领域提到的可插拔。框架
上面的类比很清晰的说明的模块化带来的好处:分布式
在《安居客 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 的实现还比较简陋,后面会进一步完善功能和文档,以后也会有单独的文章详细介绍。源码地址:github.com/BaronZ88/Ro…
对于多个 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 的组件中,在后期不断的重构迭代中视状况进行进一步的拆分。过程当中完美主义能够有,切记不可过分。
以上就是我在模块化探索实践方面的一些经验,不住之处还望你们指出。
若是你喜欢个人文章,就关注下个人 知乎专栏 或者在 GitHub 上添个 Star 吧!
- 知乎专栏:zhuanlan.zhihu.com/baron
- GitHub:github.com/BaronZ88
- 我的博客:baronzhang.com