安卓架構(gòu)之Android 模塊化/模塊化探索與實(shí)踐
來(lái)源:原創(chuàng) 時(shí)間:2017-10-19 瀏覽:0 次一、前語(yǔ)
萬(wàn)維網(wǎng)發(fā)明人 Tim Berners-Lee 談到規(guī)劃原理時(shí)說(shuō)過(guò):“簡(jiǎn)略性和模塊化是軟件工程的柱石;分布式和容錯(cuò)性是互聯(lián)網(wǎng)的生命。” 由此可見(jiàn)模塊化之于軟件工程范疇的重要性。
從 2016 年開(kāi)端,模塊化在 Android 社區(qū)越來(lái)越多的被提及。跟著移動(dòng)渠道的不斷發(fā)展,移動(dòng)渠道上的軟件漸漸走向雜亂化,體積也變得臃腫巨大;為了下降大型軟件雜亂性和耦合度,一起也為了習(xí)慣模塊重用、多團(tuán)隊(duì)并行開(kāi)發(fā)測(cè)驗(yàn)等等需求,模塊化在 Android 渠道上變得勢(shì)在必行。阿里 Android 團(tuán)隊(duì)在年初開(kāi)源了他們的容器化結(jié)構(gòu) Atlas 就很大程度闡明了當(dāng)時(shí) Android 渠道開(kāi)發(fā)大型商業(yè)項(xiàng)目所面對(duì)的問(wèn)題。
二、什么是模塊化
那么什么是模塊化呢?《 Java 運(yùn)用架構(gòu)規(guī)劃:模塊化形式與 OSGi 》一書(shū)中對(duì)它的界說(shuō)是:模塊化是一種處理雜亂體系分解為更好的可辦理模塊的辦法。
上面這種描繪過(guò)分生澀難明,不行直觀。下面這種類比的辦法則可能加簡(jiǎn)略了解。
我們能夠把軟件看做是一輛轎車,開(kāi)發(fā)一款軟件的進(jìn)程就是出產(chǎn)一輛轎車的進(jìn)程。一輛轎車由車架、發(fā)動(dòng)機(jī)、變數(shù)箱、車輪等一系列模塊組成;相同,一款大型商業(yè)軟件也是由各個(gè)不同的模塊組成的。
轎車的這些模塊是由不同的工廠出產(chǎn)的,一輛 BMW 的發(fā)動(dòng)機(jī)可能是由坐落德國(guó)的工廠出產(chǎn)的,它的主動(dòng)變數(shù)箱可能是 Jatco(國(guó)際三大變速箱廠商之一)坐落日本的工廠出產(chǎn)的,車輪可能是我國(guó)的工廠出產(chǎn)的,終究交給華晨寶馬的工廠一致組裝成一輛完好的轎車。這就相似于我們?cè)谲浖こ谭懂犂镎f(shuō)的多團(tuán)隊(duì)并行開(kāi)發(fā),終究將各個(gè)團(tuán)隊(duì)開(kāi)發(fā)的模塊一致打包成我們可運(yùn)用的 App 。
一款發(fā)動(dòng)機(jī)、一款變數(shù)箱都不行能只運(yùn)用于一個(gè)車型,比方同一款 Jatco 的 6AT 主動(dòng)變速箱既可能被安裝在 BMW 的車型上,也可能被安裝在 Mazda 的車型上。這就好像軟件開(kāi)發(fā)范疇里的模塊重用。
到了冬季,特別是在北方我們可能需求開(kāi)著車走雪路,為了安全起見(jiàn)往往我們會(huì)將轎車的公路胎晉級(jí)為雪地胎;輪胎能夠很簡(jiǎn)略的替換,這就是我們?cè)谲浖_(kāi)發(fā)范疇談到的低耦合。一個(gè)模塊的晉級(jí)替換不會(huì)影響到其它模塊,也不會(huì)受其它模塊的約束;一起這也相似于我們?cè)谲浖_(kāi)發(fā)范疇說(shuō)到的可插拔。
三、模塊化分層規(guī)劃
上面的類比很明晰的闡明的模塊化帶來(lái)的優(yōu)點(diǎn):
多團(tuán)隊(duì)并行開(kāi)發(fā)測(cè)驗(yàn);
模塊間解耦、重用;
可獨(dú)自編譯打包某一模塊,提高開(kāi)發(fā)功率。
在《安居客 Android 項(xiàng)目架構(gòu)演進(jìn)》這篇文章中,我介紹了安居客 Android 端的模塊化規(guī)劃方案,這兒我仍是拿它來(lái)舉例。但首要要對(duì)本文中的組件和模塊做個(gè)差異界說(shuō)
組件:指的是單一的功用組件,如地圖組件(MapSDK)、付出組件(AnjukePay)、路由組件(Router)等等;
模塊:指的是獨(dú)立的事務(wù)模塊,如新房模塊(NewHouseModule)、二手房模塊(SecondHouseModule)、即時(shí)通訊模塊(InstantMessagingModule)等等;模塊相關(guān)于組件來(lái)說(shuō)粒度更大。
具體規(guī)劃方案如下圖:
整個(gè)項(xiàng)目分為三層,從下至上分別是:
Basic Component Layer: 根底組件層,望文生義就是一些根底組件,包含了各種開(kāi)源庫(kù)以及和事務(wù)無(wú)關(guān)的各種自研東西庫(kù);
Business Component Layer: 事務(wù)組件層,這一層的一切組件都是事務(wù)相關(guān)的,例如上圖中的付出組件 AnjukePay、數(shù)據(jù)模仿組件 DataSimulator 等等;
Business Module Layer: 事務(wù) Module 層,在 Android Studio 中每塊事務(wù)對(duì)應(yīng)一個(gè)獨(dú)自的 Module。例如安居客用戶 App 我們就能夠拆分紅新房 Module、二手房 Module、IM Module 等等,每個(gè)獨(dú)自的 Business Module 都有必要準(zhǔn)恪守我們自己的 MVP 架構(gòu)。
我們?cè)谡勀K化的時(shí)分,其實(shí)就是將事務(wù)模塊層的各個(gè)功用事務(wù)拆分層獨(dú)立的事務(wù)模塊。所以我們進(jìn)行模塊化的第一步就是事務(wù)模塊區(qū)分,可是模塊區(qū)分并沒(méi)有一個(gè)業(yè)界通用的規(guī)范,因而區(qū)分的粒度需求依據(jù)項(xiàng)目狀況進(jìn)行合理把控,這就需求對(duì)事務(wù)和項(xiàng)目有較為透徹的了解。拿安居客來(lái)舉例,我們會(huì)將項(xiàng)目區(qū)分為新房模塊、二手房模塊、IM 模塊等等。
每個(gè)事務(wù)模塊在 Android Studio 中的都是一個(gè) Module ,因而在命名方面我們要求每個(gè)事務(wù)模塊都以 Module 為后綴。如下圖所示:
關(guān)于模塊化項(xiàng)目,每個(gè)獨(dú)自的 Business Module 都能夠獨(dú)自編譯成 APK。在開(kāi)發(fā)階段需求獨(dú)自打包編譯,項(xiàng)目發(fā)布的時(shí)分又需求它作為項(xiàng)目的一個(gè) Module 來(lái)全體編譯打包。簡(jiǎn)略的說(shuō)就是開(kāi)發(fā)時(shí)是 Application,發(fā)布時(shí)是 Library。因而需求在 Business Module 的 build.gradle 中參加如下代碼:
if
(isBuildModule.toBoolean()){
apply plugin:
'com.android.application'
}
else
{
apply plugin:
'com.android.library'
}
isBuildModule 在項(xiàng)目根目錄的 gradle.properties 中界說(shuō):
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"
/>
一起針對(duì)模塊化我們也界說(shuō)了一些自己的游戲規(guī)矩:
關(guān)于 Business Module Layer,各事務(wù)模塊之間不允許存在相互依靠聯(lián)系,它們之間的跳轉(zhuǎn)通訊選用路由結(jié)構(gòu) Router 來(lái)完成(后邊會(huì)介紹 Router 結(jié)構(gòu)的完成);
關(guān)于 Business Component Layer,單一事務(wù)組件只能對(duì)應(yīng)某一項(xiàng)具體的事務(wù),個(gè)性化需求對(duì)外部供給接口讓調(diào)用方定制;
合理操控各組件和各事務(wù)模塊的拆分粒度,太小的公有模塊不足以構(gòu)成獨(dú)自組件或許模塊的,我們先放到相似于 CommonBusiness 的組件中,在后期不斷的重構(gòu)迭代中視狀況進(jìn)行進(jìn)一步的拆分;
上層的公有事務(wù)或許功用模塊能夠逐漸下放到基層,合理掌握好度就好;
各 Layer 間禁止反向依靠,橫向依靠聯(lián)系由各事務(wù) Leader 和技能小組參議決議。
四、模塊間跳轉(zhuǎn)通訊(Router)
對(duì)事務(wù)進(jìn)行模塊化拆分后,為了使各事務(wù)模塊間解耦,因而各個(gè) Bussiness Module 都是獨(dú)立的模塊,它們之間是沒(méi)有依靠聯(lián)系。那么各個(gè)模塊間的跳轉(zhuǎn)通訊怎么完成呢?
比方事務(wù)上要求從新房的列表頁(yè)跳轉(zhuǎn)到二手房的列表頁(yè),那么由所以 NewHouseModule 和 SecondHouseModule 之間并不相互依靠,我們經(jīng)過(guò)想如下這種顯式跳轉(zhuǎn)的辦法來(lái)完成 Activity 跳轉(zhuǎn)顯然是不行能的完成的。
Intent
intent =
new
Intent
(
NewHouseListActivity
.
this
,
SecondHouseListActivity
.
class
);
startActivity(intent);
有的同學(xué)可能會(huì)想到用隱式跳轉(zhuǎn),經(jīng)過(guò) Intent 匹配規(guī)矩來(lái)完成:
Intent
intent =
new
Intent
(
Intent
.ACTION_VIEW,
"://:/"
);
startActivity(intent);
可是這種代碼寫起來(lái)比較繁瑣,且簡(jiǎn)略犯錯(cuò),犯錯(cuò)也不太簡(jiǎn)略定位問(wèn)題。因而一個(gè)簡(jiǎn)略易用、解放開(kāi)發(fā)的路由結(jié)構(gòu)是有必要的了。
我自己完成的路由結(jié)構(gòu)分為路由(Router)和參數(shù)注入器(Injector)兩部分:
Router 供給 Activity 跳轉(zhuǎn)傳參的功用;Injector 供給參數(shù)注入功用,經(jīng)過(guò)編譯時(shí)生成代碼的辦法在 Activity 獲取獲取傳遞過(guò)來(lái)的參數(shù),簡(jiǎn)化開(kāi)發(fā)。
4.1 Router
路由(Router)部分經(jīng)過(guò) Java 注解結(jié)合動(dòng)態(tài)署理來(lái)完成,這一點(diǎn)和 Retrofit 的完成原理是一樣的。
首要需求界說(shuō)我們自己的注解(篇幅有限,這兒只列出少部分源碼)。
用于界說(shuō)跳轉(zhuǎn) URI 的注解 FullUri:
@Target
(
ElementType
.METHOD)
@Retention
(
RetentionPolicy
.RUNTIME)
public
@interface
FullUri
{
String
value();
}
用于界說(shuō)跳轉(zhuǎn)傳參的 UriParam( UriParam 注解的參數(shù)用于拼接到 URI 后邊):
@Target
(
ElementType
.PARAMETER)
@Retention
(
RetentionPolicy
.RUNTIME)
public
@interface
UriParam
{
String
value();
}
用于界說(shuō)跳轉(zhuǎn)傳參的 IntentExtrasParam( IntentExtrasParam 注解的參數(shù)終究經(jīng)過(guò) Intent 來(lái)傳遞):
@Target
(
ElementType
.PARAMETER)
@Retention
(
RetentionPolicy
.RUNTIME)
public
@interface
IntentExtrasParam
{
String
value();
}
然后完成 Router ,內(nèi)部經(jīng)過(guò)動(dòng)態(tài)署理的辦法來(lái)完成 Activity 跳轉(zhuǎn):
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());
//獲取注解參數(shù)
Annotation
[][] parameterAnnotations = method.getParameterAnnotations();
HashMap
<
String
,
Object
> serializedParams =
new
HashMap
<>();
//拼接跳轉(zhuǎn) 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 后的參數(shù)
...
}
else
if
(annotation
instanceof
IntentExtrasParam
) {
//Intent 傳參處理
...
}
}
//履行Activity跳轉(zhuǎn)操作
performJump(urlBuilder.toString(), serializedParams);
return
null
;
}
});
}
...
}
上面是 Router 完成的部分代碼,在運(yùn)用 Router 來(lái)跳轉(zhuǎn)的時(shí)分,首要需求界說(shuō)一個(gè) Interface(相似于 Retrofit 的運(yùn)用辦法):
public
interface
RouterService
{
@FullUri
(
"router://com.baronzhang.android.router.FourthActivity"
)
void
startUserActivity(
@UriParam
(
"cityName"
)
String
cityName,
@IntentExtrasParam
(
"user"
)
User
user);
}
接下來(lái)我們就能夠經(jīng)過(guò)如下辦法完成 Activity 的跳轉(zhuǎn)傳參了:
RouterService
routerService =
new
Router
(
this
).create(
RouterService
.
class
);
User
user =
new
User
(
"張三"
,
17
,
165
,
88
);
routerService.startUserActivity(
"上海"
, user);
4.2 Injector
經(jīng)過(guò) Router 跳轉(zhuǎn)到方針 Activity 后,我們需求在方針 Activity 中獲取經(jīng)過(guò) Intent 傳過(guò)來(lái)的參數(shù):
getIntent().getIntExtra(
"intParam"
,
0
);
getIntent().getData().getQueryParameter(
"preActivity"
);
為了簡(jiǎn)化這部分作業(yè),路由結(jié)構(gòu) Router 中供給了 Injector 模塊在編譯時(shí)生成上述代碼。參數(shù)注入器(Injector)部分經(jīng)過(guò) Java 編譯時(shí)注解來(lái)完成,完成思路和 ButterKnife 這類編譯時(shí)注解結(jié)構(gòu)相似。
首要界說(shuō)我們的參數(shù)注解 InjectUriParam :
@Target
(
ElementType
.FIELD)
@Retention
(
RetentionPolicy
.CLASS)
public
@interface
InjectUriParam
{
String
value()
default
""
;
}
然后完成一個(gè)注解處理器 InjectProcessor ,在編譯階段生成獲取參數(shù)的代碼:
@AutoService
(
Processor
.
class
)
public
class
InjectProcessor
extends
AbstractProcessor
{
...
@Override
public
boolean
process(
Set
extends
TypeElement
> set,
RoundEnvironment
roundEnvironment) {
//解析注解
Map
<
TypeElement
,
TargetClass
> targetClassMap = findAndParseTargets(roundEnvironment);
//解析完成后,生成的代碼的結(jié)構(gòu)已經(jīng)有了,它們存在InjectingClass中
for
(
Map
.
Entry
<
TypeElement
,
TargetClass
> entry : targetClassMap.entrySet()) {
...
}
return
false
;
}
...
}
運(yùn)用辦法相似于 ButterKnife ,在 Activity 中我們運(yùn)用 Inject 來(lái)注解一個(gè)全局變量:
@Inject
User
user;
然后 onCreate 辦法中需求調(diào)用 inject(Activity activity) 辦法完成注入:
RouterInjector
.inject(
this
);
這樣我們就能夠獲取到前面經(jīng)過(guò) Router 跳轉(zhuǎn)的傳參了。
因?yàn)槠s束,加上為了便于了解,這兒只貼出了很少部分 Router 結(jié)構(gòu)的源碼。期望進(jìn)一步了解 Router 完成原理的能夠到 GiuHub 去翻閱源碼,Router 的完成還比較粗陋,后邊會(huì)進(jìn)一步完善功用和文檔,之后也會(huì)有獨(dú)自的文章具體介紹。源碼地址:https://github.com/BaronZ88/Router
五、問(wèn)題及主張
5.1 資源名抵觸
關(guān)于多個(gè) Bussines Module 中資源名抵觸的問(wèn)題,能夠經(jīng)過(guò)在 build.gradle 界說(shuō)前綴的辦法處理:
defaultConfig {
...
resourcePrefix
"new_house_"
...
}
而關(guān)于 Module 中有些資源不想被外部拜訪的,我們能夠創(chuàng)立 res/values/public.xml,增加到 public.xml 中的 resource 則可被外部拜訪,未增加的則視為私有:
name
=
"new_house_settings"
type
=
"string"
/>
5.2 重復(fù)依靠
模塊化的進(jìn)程中我們常常會(huì)遇到重復(fù)依靠的問(wèn)題,如果是經(jīng)過(guò) aar 依靠, gradle 會(huì)主動(dòng)幫我們找出新版別,而扔掉老版別的重復(fù)依靠。如果是以 project 的辦法依靠,則在打包的時(shí)分會(huì)呈現(xiàn)重復(fù)類。關(guān)于這種狀況我們能夠在 build.gradle 中將 compile 改為 provided,只在終究的項(xiàng)目中 compile 對(duì)應(yīng)的 library ;
其實(shí)早年面的安居客模塊化規(guī)劃圖上能看出來(lái),我們的規(guī)劃方案能必定程度上躲避重復(fù)依靠的問(wèn)題。比方我們一切的第三方庫(kù)的依靠都會(huì)放到 OpenSoureLibraries 中,其他需求用到相關(guān)類庫(kù)的項(xiàng)目,只需求依靠 OpenSoureLibraries 就好了。
5.3 模塊化進(jìn)程中的主張
關(guān)于大型的商業(yè)項(xiàng)目,在重構(gòu)進(jìn)程中可能會(huì)遇到事務(wù)耦合嚴(yán)峻,難以拆分的問(wèn)題。我們需求先理清事務(wù),再著手拆分事務(wù)模塊。比方能夠先在原先的項(xiàng)目中依據(jù)事務(wù)分包,在必定程度大將各事務(wù)解耦后拆分到不同的 package 中。比方之前新房和二手房因?yàn)橥瑲w于 app module,因而他們之前是經(jīng)過(guò)隱式的 intent 跳轉(zhuǎn)的,現(xiàn)在能夠先將他們改為經(jīng)過(guò) Router 來(lái)完成跳轉(zhuǎn)。又比方新房和二手房中共用的模塊能夠先下放到 Business Component Layer 或許 Basic Component Layer 中。在這一系列作業(yè)完成后再將各個(gè)事務(wù)拆分紅多個(gè) module 。
模塊化重構(gòu)需求漸進(jìn)式的打開(kāi),不行一觸而就,不要想著將整個(gè)項(xiàng)目推翻重寫。線上老練安穩(wěn)的事務(wù)代碼,是經(jīng)過(guò)了時(shí)刻和很多用戶檢測(cè)的;悉數(shù)推翻重寫往往費(fèi)時(shí)吃力,實(shí)踐的作用一般也很不抱負(fù),各種問(wèn)題層出不窮因小失大。關(guān)于這種項(xiàng)目的模塊化重構(gòu),我們需求一點(diǎn)點(diǎn)的改善重構(gòu),能夠渙散到每次的事務(wù)迭代中去,逐漸篩選掉陳腐的代碼。
各事務(wù)模塊間必定會(huì)有共用的部分,依照我前面的規(guī)劃圖,共用的部分我們會(huì)依據(jù)事務(wù)相關(guān)性下放到事務(wù)組件層(Business Component Layer)或許根底組件層(Common Component Layer)。關(guān)于太小的公有模塊不足以構(gòu)成獨(dú)自組件或許模塊的,我們先放到相似于 CommonBusiness 的組件中,在后期不斷的重構(gòu)迭代中視狀況進(jìn)行進(jìn)一步的拆分。進(jìn)程中完美主義能夠有,牢記不行過(guò)度。