安卓架构之Android 模块化/模块化探索与实践
来源:原创 时间:2017-10-19 浏览:0 次一、前语
万维网发明人 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)等等;模块相关于组件来说粒度更大。
具体规划方案如下图:
整个项目分为三层,从下至上分别是:
Basic Component Layer: 根底组件层,望文生义就是一些根底组件,包含了各种开源库以及和事务无关的各种自研东西库;
Business Component Layer: 事务组件层,这一层的一切组件都是事务相关的,例如上图中的付出组件 AnjukePay、数据模仿组件 DataSimulator 等等;
Business Module Layer: 事务 Module 层,在 Android Studio 中每块事务对应一个独自的 Module。例如安居客用户 App 我们就能够拆分红新房 Module、二手房 Module、IM Module 等等,每个独自的 Business Module 都有必要准恪守我们自己的 MVP 架构。
我们在谈模块化的时分,其实就是将事务模块层的各个功用事务拆分层独立的事务模块。所以我们进行模块化的第一步就是事务模块区分,可是模块区分并没有一个业界通用的规范,因而区分的粒度需求依据项目状况进行合理把控,这就需求对事务和项目有较为透彻的了解。拿安居客来举例,我们会将项目区分为新房模块、二手房模块、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 :
>
android:name
=
"com.baronzhang.android.newhouse.NewHouseMainActivity"
android:label
=
"@string/new_house_label_home_page"
>
android:name
=
"android.intent.action.MAIN"
/>
android:name
=
"android.intent.category.LAUNCHER"
/>
realease 形式下的 AndroidManifest.xml :
>
android:name
=
"com.baronzhang.android.newhouse.NewHouseMainActivity"
android:label
=
"@string/new_house_label_home_page"
>
android:name
=
"android.intent.category.DEFAULT"
/>
android:name
=
"android.intent.category.BROWSABLE"
/>
android:name
=
"android.intent.action.VIEW"
/>
android:host
=
"com.baronzhang.android.newhouse"
android:scheme
=
"router"
/>
一起针对模块化我们也界说了一些自己的游戏规矩:
关于 Business Module Layer,各事务模块之间不允许存在相互依靠联系,它们之间的跳转通讯选用路由结构 Router 来完成(后边会介绍 Router 结构的完成);
关于 Business Component Layer,单一事务组件只能对应某一项具体的事务,个性化需求对外部供给接口让调用方定制;
合理操控各组件和各事务模块的拆分粒度,太小的公有模块不足以构成独自组件或许模块的,我们先放到相似于 CommonBusiness 的组件中,在后期不断的重构迭代中视状况进行进一步的拆分;
上层的公有事务或许功用模块能够逐渐下放到基层,合理掌握好度就好;
各 Layer 间禁止反向依靠,横向依靠联系由各事务 Leader 和技能小组参议决议。
四、模块间跳转通讯(Router)
对事务进行模块化拆分后,为了使各事务模块间解耦,因而各个 Bussiness Module 都是独立的模块,它们之间是没有依靠联系。那么各个模块间的跳转通讯怎么完成呢?
比方事务上要求从新房的列表页跳转到二手房的列表页,那么由所以 NewHouseModule 和 SecondHouseModule 之间并不相互依靠,我们经过想如下这种显式跳转的办法来完成 Activity 跳转显然是不行能的完成的。
Intent
intent =
new
Intent
(
NewHouseListActivity
.
this
,
SecondHouseListActivity
.
class
);
startActivity(intent);
有的同学可能会想到用隐式跳转,经过 Intent 匹配规矩来完成:
Intent
intent =
new
Intent
(
Intent
.ACTION_VIEW,
"://:/"
);
startActivity(intent);
可是这种代码写起来比较繁琐,且简略犯错,犯错也不太简略定位问题。因而一个简略易用、解放开发的路由结构是有必要的了。
我自己完成的路由结构分为路由(Router)和参数注入器(Injector)两部分:
Router 供给 Activity 跳转传参的功用;Injector 供给参数注入功用,经过编译时生成代码的办法在 Activity 获取获取传递过来的参数,简化开发。
4.1 Router
路由(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 create(
final
Class
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);
4.2 Injector
经过 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 的完成还比较粗陋,后边会进一步完善功用和文档,之后也会有独自的文章具体介绍。源码地址:https://github.com/BaronZ88/Router
五、问题及主张
5.1 资源名抵触
关于多个 Bussines Module 中资源名抵触的问题,能够经过在 build.gradle 界说前缀的办法处理:
defaultConfig {
...
resourcePrefix
"new_house_"
...
}
而关于 Module 中有些资源不想被外部拜访的,我们能够创立 res/values/public.xml,增加到 public.xml 中的 resource 则可被外部拜访,未增加的则视为私有:
name
=
"new_house_settings"
type
=
"string"
/>
5.2 重复依靠
模块化的进程中我们常常会遇到重复依靠的问题,如果是经过 aar 依靠, gradle 会主动帮我们找出新版别,而扔掉老版别的重复依靠。如果是以 project 的办法依靠,则在打包的时分会呈现重复类。关于这种状况我们能够在 build.gradle 中将 compile 改为 provided,只在终究的项目中 compile 对应的 library ;
其实早年面的安居客模块化规划图上能看出来,我们的规划方案能必定程度上躲避重复依靠的问题。比方我们一切的第三方库的依靠都会放到 OpenSoureLibraries 中,其他需求用到相关类库的项目,只需求依靠 OpenSoureLibraries 就好了。
5.3 模块化进程中的主张
关于大型的商业项目,在重构进程中可能会遇到事务耦合严峻,难以拆分的问题。我们需求先理清事务,再着手拆分事务模块。比方能够先在原先的项目中依据事务分包,在必定程度大将各事务解耦后拆分到不同的 package 中。比方之前新房和二手房因为同归于 app module,因而他们之前是经过隐式的 intent 跳转的,现在能够先将他们改为经过 Router 来完成跳转。又比方新房和二手房中共用的模块能够先下放到 Business Component Layer 或许 Basic Component Layer 中。在这一系列作业完成后再将各个事务拆分红多个 module 。
模块化重构需求渐进式的打开,不行一触而就,不要想着将整个项目推翻重写。线上老练安稳的事务代码,是经过了时刻和很多用户检测的;悉数推翻重写往往费时吃力,实践的作用一般也很不抱负,各种问题层出不穷因小失大。关于这种项目的模块化重构,我们需求一点点的改善重构,能够涣散到每次的事务迭代中去,逐渐筛选掉陈腐的代码。
各事务模块间必定会有共用的部分,依照我前面的规划图,共用的部分我们会依据事务相关性下放到事务组件层(Business Component Layer)或许根底组件层(Common Component Layer)。关于太小的公有模块不足以构成独自组件或许模块的,我们先放到相似于 CommonBusiness 的组件中,在后期不断的重构迭代中视状况进行进一步的拆分。进程中完美主义能够有,牢记不行过度。