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