您的位置:首页 >  新闻中心 > 行业动态
  行业动态
 

安卓架构之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 的组件中,在后期不断的重构迭代中视状况进行进一步的拆分。进程中完美主义能够有,牢记不行过度。