項目背景
主站業(yè)務(wù)經(jīng)歷了長期的迭代維護辕录,業(yè)務(wù)的增長同時帶來每個版本業(yè)務(wù)量繁重碉考,迭代周期很快轿曙。同時團隊也在不斷的擴張,對應(yīng)拆分了組內(nèi)不同的業(yè)務(wù)線對接不同業(yè)務(wù)線的需求,最初的Android客戶端單一的設(shè)計架構(gòu)已經(jīng)逐漸不滿足快速的業(yè)務(wù)開發(fā)需求。歷經(jīng)組內(nèi)討論開始對項目整理進行組件化的遷移统诺,通過組件化的方式滿足不同業(yè)務(wù)線業(yè)務(wù)開發(fā)的穩(wěn)定性皮璧,是迭代開發(fā)更靈活,組內(nèi)協(xié)作開發(fā)效率得到提升遥巴。同時又有新的項目立項需要投入開發(fā)千康,一方面可以通過新項目實踐和推進組件化的遷移,另一方面也可以通過組件化拆分后的技術(shù)組件復(fù)用來更快的搭建和開發(fā)新的項目铲掐。
組件化的準(zhǔn)備
技術(shù)準(zhǔn)備
主站最初的app項目只有一個模塊拾弃,業(yè)務(wù)耦合嚴(yán)重,技術(shù)組件很難復(fù)用摆霉,所以我們采取的第一步是拆分部分基礎(chǔ)組件下沉為一個Base庫豪椿,盡量去解耦業(yè)務(wù)提取基礎(chǔ)技術(shù)組件達(dá)到多業(yè)務(wù)模塊的復(fù)用奔坟,也是為了支持新項目和主站項目多個app的技術(shù)支持。
考慮組件化后的業(yè)務(wù)相對隔離搭盾,但是客戶端組件間需要建立訪問咳秉,所以需要組件間通信的介入。我們采取的方式是路由鸯隅、服務(wù)和全局通知澜建。
搭建路由庫支持,目的是解決業(yè)務(wù)組件物理隔離后的UI跳轉(zhuǎn)和訪問蝌以,通過維護路由表的方式尋址到需要訪問的業(yè)務(wù)組件UI炕舵。我們采取的是技術(shù)實現(xiàn)是通過注解給對應(yīng)的業(yè)務(wù)UI比如LoginActivity上用注解申明對應(yīng)的路由地址,在公共依賴的接口處公開維護這個路由地址常量跟畅,暴露給其他業(yè)務(wù)組件通過方位該地址來跳轉(zhuǎn)到對應(yīng)的業(yè)務(wù)組件UI咽筋。
@Router(RouterPath.LOGIN_PAGE)
public class LoginActivity extends BaseCompatActivity
public class RouterPath {
/**
* 登錄
*/
public static final String LOGIN_PAGE = "/native/xxx-login\\.html";
/**
* 搜索key
*/
public static final String SEARCH_KEY = "/native/xxx-search-key\\.html";
...
}
對應(yīng)Act綁定上路由地址后,需要對路由的地址進行統(tǒng)一的收集管理徊件。同時也為了支持某些服務(wù)動態(tài)下發(fā)的地址晤硕,策略是優(yōu)先在本地的路由表進行匹配,如果查詢到了該地址有對應(yīng)的Native界面優(yōu)先跳轉(zhuǎn)到Native的界面庇忌,未匹配到則跳轉(zhuǎn)到由webView容器承載的網(wǎng)頁舞箍。目前我們采取的方式是通過APT自動生成對應(yīng)路由注解后的activity的收集類。
//自動生成的類皆疹,命名規(guī)則是RouterGenerator+業(yè)務(wù)組件模塊名稱
//RouterGenerator_login.class
public class RouterGenerator_login implements RouterProvider {
public RouterGenerator_login() {
}
public void loadRouter(Map<String, Route> routerMap, Map<String, Route> pageNameRouterMap) {
String keyLoginActivity = "((https|http|domain|native)://(\\w+\\.)?domain\\.com/native/domain-login\\.html)|(" + RouteBuilder.generateUriFromClazz(LoginActivity.class) + ")";
routerMap.put(keyLoginActivity, RouteBuilder.build(keyLoginActivity, 0, false, (String[])null, LoginActivity.class));
}
}
然后再通過ASM的方式在編譯期對所有加載到工程里面的模塊組件通過特定的規(guī)則進行上面路由輔助類的收集疏橄。
//收集路由地址
[
'scanInterface' : 'com.kaola.annotation.provider.RouterProvider',
'scanSuperClasses' : [],
'codeInsertToClassName': 'com.kaola.core.center.router.RouterMap',
//未指定codeInsertToMethodName,默認(rèn)插入到static塊中略就,故此處register必須為static方法
'registerMethodName' : 'register',
'include' : ['com/kaola/annotation/provider/result/.*'
]
//根據(jù)工程依賴的所有組件模塊收集所有實現(xiàn)RouterProvider的輔助類捎迫。
//然后插入到RouterMap的靜態(tài)代碼塊中,默認(rèn)調(diào)用無參構(gòu)造表牢。
//遍歷執(zhí)行RouterMap中的靜態(tài)方法register窄绒,添加所有路由地址信息到全局路由表sRouterMap中。
public class RouterMap {
private static Map<String, Route> sRouterMap = new ConcurrentHashMap<>();
private static Map<String, Route> sPageRouterMap = new ConcurrentHashMap<>();
private static void register(RouterProvider routerProvider) {
routerProvider.loadRouter(sRouterMap, sPageRouterMap);
}
}
具體實現(xiàn)不再此展開了崔兴,此方式的好處就是可以根據(jù)需求加載需要的業(yè)務(wù)組件并且實現(xiàn)自動注冊和收集路由到路由表彰导。如果覺得獨立開發(fā)路由庫的成本較高,也可以采取業(yè)界主流的一些路由庫比如ARouter等敲茄,基本類似位谋。
- 關(guān)于組件間服務(wù)通信的方式,目前采取的是暴露對應(yīng)的服務(wù)接口供各個業(yè)務(wù)組件方調(diào)用堰燎。每個業(yè)務(wù)組件都會申明需要對外暴露提供的方法掏父,并在自己的業(yè)務(wù)組件模塊內(nèi)實現(xiàn)這些具體被調(diào)用的方法。對外接口庫根據(jù)模塊劃分秆剪,可以申明和維護通信間的一些數(shù)據(jù)類型赊淑,比如公開的數(shù)據(jù)model和對應(yīng)需要訪問的一些路由地址等爵政。為了便于服務(wù)的動態(tài)收集,這些服務(wù)接口可以統(tǒng)一的繼承某個規(guī)則接口陶缺,然后采取上述路由的方式钾挟,對所有實現(xiàn)了該規(guī)則接口的服務(wù)接口統(tǒng)一的收集管理。
facade/pay/
model/PayModel.class
IPayService.class
//IService.class组哩,統(tǒng)一對繼承IService的服務(wù)接口的具體實現(xiàn)類進行收集
interface PayService : IService {
fun startH5PaySercive(context: Context)
}
pay_module/
PayServiceImpl.class
class PayServiceImpl : PayService {
override fun startH5PaySercive(context: Context) {
//...
}
}
剩下一些特點場景的業(yè)務(wù)等龙,比如:登錄成功后需要全局通知刷新多個UI某個業(yè)務(wù)狀態(tài)的時候,目前采取EventBus的方式進行訂閱通知伶贰。
- 在組件base庫一定下沉和組件間通信方式的確立蛛砰,開始對組件的具體的拆分粒度進行劃分。大致劃分為業(yè)務(wù)組件和技術(shù)組件兩部分黍衙。
組件化的拆分流程
拆分前的考慮
考慮新的項目投入的人力資源有限泥畅,并且需要快速的開發(fā)上線,同時業(yè)務(wù)也有重合的場景琅翻。所以當(dāng)時采取的開發(fā)策略是將主站未組件化的代碼完全拷貝一份到新項目位仁,并在此的基礎(chǔ)上進行改造。改造的原則必須遵循2個應(yīng)用共建同一套BaseLib方椎,但是由于主站的BaseLib里面會耦合一些自身的業(yè)務(wù)組件聂抢,同時避免對BaseLib的修改影響到主站的業(yè)務(wù)開發(fā)而增加不必要的工作量。當(dāng)時采取的策略是通過增加一層業(yè)務(wù)基礎(chǔ)組件庫來做新項目組件化拆分的緩沖層BaseCompatLib棠众。
拆分過程
拆分過程中有很多業(yè)務(wù)組件共用的情況琳疏,結(jié)合當(dāng)時的開發(fā)周期可以適當(dāng)?shù)娜ソ怦畈糠謽I(yè)務(wù)組件重新劃分到對應(yīng)拆分后的業(yè)務(wù)模塊中。如果時間有限闸拿,可以先挪到BaseCompatLib這個緩沖成暫時共用待后續(xù)再拆空盼,從而避免對2個項目共用的Base庫頻繁修改帶來的負(fù)擔(dān)。
初期的業(yè)務(wù)模塊獨立編譯的配置方式新荤,僅供參考:
//gradle.properties中申明編譯配置是否是獨立編譯
# Module Build
isModuleInjectBuild=true
//moduleLibrary的build.gradle中申明編譯方式
if (isModuleInjectBuild.toBoolean()) {
apply from: '../build_module.gradle'
} else {
apply from: '../build_app.gradle'
}
//新建一個appbuild文件揽趾,用來支業(yè)務(wù)組件以app方式編譯時所需的配置
//示例:
java/appbuild/
BuildInfo.class //獨立配置
HomeServiceImpl.class //改寫應(yīng)用啟動跳轉(zhuǎn)的UI
App.class //獨立編譯時的application,用于初始化配置
android {
//配置源碼路徑
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jnilibs']
//如果是整體編譯苛骨,可以移除獨立編譯所需的額外代碼
if (isModuleInjectBuild.toBoolean()) {
java {
exclude 'appbuild/**'
}
}
}
}
}
遇到的問題
拆分后的獨立模塊由于一些基礎(chǔ)服務(wù)的初始化仍停留在app殼工程篱瞎,一些sdk或者初始化服務(wù)沒有統(tǒng)一的管理。優(yōu)先級混亂并且耦合大量的業(yè)務(wù)邏輯智袭,導(dǎo)致業(yè)務(wù)模塊拆分后無法獨立運行奔缠,缺失對應(yīng)組件所需服務(wù)的初始化步驟。開始改造初始化的業(yè)務(wù)吼野,原理同自動收集一致。
interface IInitializer {
fun loadInQueue(queue: PriorityQueue<InitialTask>) //收集需要的服務(wù)進隊列
fun init(processName: String) //對應(yīng)初始化服務(wù)的實現(xiàn)
}
class InitialManager {
companion object {
private val mInitializerQueue = PriorityQueue<InitialTask>() //服務(wù)隊列
private var mCurProcessName: String = "" //當(dāng)前啟動的進程
//應(yīng)用初始化時的調(diào)用的入口函數(shù)
@JvmStatic
fun initial(curProcessName: String) {
mCurProcessName = curProcessName
initialInProcess()
}
@JvmStatic
fun initialInProcess() {
loop@ while (mInitializerQueue.isNotEmpty()) { //搜索接入了多少三方sdk功能两波,總?cè)蝿?wù)隊列
val initialTask = mInitializerQueue.poll() //按優(yōu)先級取
//根據(jù)是否擁有權(quán)限去加載普通任務(wù)
//特殊不需要檢查權(quán)限的任務(wù)瞳步,包括:Config和Permission初始化本身的任務(wù)闷哆。
//目前這些優(yōu)先級必須高于普通任務(wù),否則會被提前打斷单起,等到權(quán)限獲取后才會執(zhí)行抱怔。
when {
PermissionUtils.isNecessaryPermissionGranted() || initialTask.isNoNeedPermissionCheck() -> {
executeTask(initialTask)
}
else -> {
//一旦被權(quán)限檢查打斷不能執(zhí)行,取出的任務(wù)重新放回隊列嘀倒。跳出任務(wù)隊列屈留,等待權(quán)限獲取后的再次執(zhí)行。
mInitializerQueue.add(initialTask)
break@loop
}
}
}
}
/**
* 執(zhí)行任務(wù)测蘑,匹配對應(yīng)進程灌危,對應(yīng)進程啟動對應(yīng)需要初始化的任務(wù),沿用主站的邏輯
*/
private fun executeTask(initialTask: InitialTask) {
initialTask.processName.forEach {
//當(dāng)前進程和服務(wù)需要初始化的進程相匹配或者是全進程需要就加載
if (it == mCurProcessName || it == InitialTask.INITIAL_ALL_PROCESS) {
Log.d("InitialManager", "initial - process:$mCurProcessName & initialTask:${initialTask.initialName}")
initialTask.initializer.init(mCurProcessName)
return@forEach
}
}
}
@JvmStatic
fun register(initializer: IInitializer) {
initializer.loadInQueue(mInitializerQueue)
}
}
//示例服務(wù)
class QiyuSdkInitial : IInitializer {
override fun loadInQueue(queue: PriorityQueue<InitialTask>) {
//主進程需要
val initialTask = InitialTask(
processName = mutableListOf(ProcessConst.MAIN_PROCESS, ProcessConst.NIM_PROCESS),
initialName = this::class.java.simpleName,
initializer = this
)
queue.add(initialTask)
}
override fun init(processName: String) {
try {
QiyuSdk.initUnicorn(AppDelegate.sApplication)
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
徹底組件化
架構(gòu)圖
組件庫的獨立發(fā)布和維護
原有拆分的本地組件徹底分離出去碳胳,采取獨立發(fā)布和維護的方式迭代更新勇蝙。
新建git倉庫和本地組件項目,然后以module的方式將原有項目中的業(yè)務(wù)module導(dǎo)入到本地新建的項目中挨约。推送該項目到git的獨立倉庫味混。(目前未采取git subModule的 方式管理,但大致差不多)
新建的本地項目中再新建一個對應(yīng)的接口工程用于對外暴露模塊中的業(yè)務(wù)訪問诫惭。
project: component-login
/app //殼工程
/login //登錄模塊module
/login-facade //登錄模塊接口module
添加打包aar發(fā)布到maven倉庫的腳本用來獨立發(fā)布login和login-facade模塊翁锡。
遵循對應(yīng)的發(fā)布規(guī)范,不同項目的app殼工程根據(jù)自身的業(yè)務(wù)需求進行對應(yīng)的組件依賴夕土,版本開發(fā)階段可采取snapshot的進行依賴馆衔。不同的業(yè)務(wù)組件也可以通過依賴其他不同的業(yè)務(wù)組件接口達(dá)到訪問的目的。(如需實際運行隘弊,不光需要再接入接口庫還需要依賴對應(yīng)的組件工程)
目前友品采取的是jenkins的打包發(fā)布方式哈踱,僅供參考。
本地開發(fā)調(diào)試模式
在組件開發(fā)過程中梨熙,單純的依靠遠(yuǎn)程方式依賴开镣,對開發(fā)階段的頻繁修改不友好。所以我們采取依賴覆蓋的方式咽扇,讓原有的依賴在編譯過程中替換掉遠(yuǎn)程的版本改用本地的版本進行引用邪财。
// 自定義const.gradle環(huán)境聲明
def version = '1.5.11'
ext.sdk = [
YpBase : { "com.kaola:ypbase:${version}" }
]
//app build.gradle
dependencies {
api gradle.sdk.YpBase(this)
}
//setting.gradle
gradle.ext {
sdk = sdk
}
//本地依賴時需要修改為本地的路徑
def YpBase_PATH = "localpath/base"
def YpBase_as_aar = []
def YpBase_as_sources = [
['YpBase', ":base", ['type': 'project', 'path': "${YpBase_PATH}/base"]],
]
def overrideList = YpBase_as_aar
// *核心* 打開注釋使用源碼引入YpBase
overrideList = YpBase_as_sources
def overrideLibrary(Map define, String whichLibrary, String name, Map prjType) {
def overrideType = prjType.get("type")
if (overrideType == 'module') {
include(name)
define.put(whichLibrary, {
it.project(name)
})
} else if (overrideType == 'project') {
include(name)
project(name).projectDir = new File(prjType.get('path'))
define.put(whichLibrary, {
it.project(name)
})
} else if (overrideType == 'aar') {
define.put(whichLibrary, { prjType.get('path') })
} else {
; // ignore
}
}
for (int i = 0; i < overrideList.size(); i++) {
def override = overrideList[i]
println 'override: ' + override[0]
overrideLibrary(sdk, override[0], override[1], override[2])
}
通過以上的方式讓Base的依賴從遠(yuǎn)程替換為本地module的形式。開發(fā)階段就可以通過AS的refactor進行代碼的優(yōu)化和重構(gòu)质欲,對本地Base修改后到Base的git分支進行對應(yīng)的提交或MR合回主分支然后走規(guī)范的發(fā)布打包流程树埠。
組件版本依賴管理
組件項目中會有對Base或者接口庫的引用,對于Base我們可以選擇compileOnly的方式嘶伟,也可以選擇直接依賴的方式怎憋。在集成到項目中后依賴會遵循gradle的依賴傳遞原則。特別注意:
避免環(huán)形依賴的產(chǎn)生。比如:facade -> base绊袋, base -> facade毕匀。遇到這種情況需要拆分所需依賴到另外一層。
在遠(yuǎn)程依賴替換為本地依賴做開發(fā)修改時可能會遇到遠(yuǎn)程依賴和本地依賴的沖突癌别。比如:app -> login -> com.xxx:base; app -> home -> /localpath/base皂岔。 此時可以采取下面的方式進行依賴優(yōu)先選擇本地的方式排除掉其他組件中的遠(yuǎn)程依賴。
//setting.gradle
def base_exist = false
for (int i = 0; i < overrideList.size(); i++) {
def override = overrideList[i]
println 'override: ' + override[0]
if (override[0] == 'YpBase') {
base_exist = true
}
overrideLibrary(sdk, override[0], override[1], override[2])
}
gradle.ext {
kulabase_exist = base_exist
}
//app.gradle
if (gradle.kulabase_exist) {
println 'kulabase_exist exist, exclude all aar dependences'
android {
configurations {
all*.exclude group: 'com.xxx', module: 'base'
}
}
}
后續(xù)
到此為止基本上組件化就可以持續(xù)穩(wěn)定的開發(fā)和維護了展姐,組件化后也給團隊的開發(fā)效率帶來一定的提升躁垛,代碼也可以在一定可控的范圍內(nèi)穩(wěn)定的維護。并且在各自維護的組件中圾笨,大家也可以根據(jù)各自需求選擇合適自己業(yè)務(wù)的開發(fā)框架比如:mvp教馆、LiveData、Rx等或者嘗試使用新語言Kotlin去編寫墅拭。解決業(yè)務(wù)耦合帶來的負(fù)擔(dān)同時也使各個組件達(dá)到了較高的可復(fù)用性活玲,靈活的支持不同的應(yīng)用項目,達(dá)到可插拔的方式集成開發(fā)谍婉。后續(xù)項目也會做一些優(yōu)化舒憾,針對版本依賴的管理和簡化組件編譯和發(fā)布集成的流程來提高協(xié)作開發(fā)的效率。