android app.build文件_网易友品 Android 客户端组件化演进
項目背景
主站業務經歷了長期的迭代維護,業務的增長同時帶來每個版本業務量繁重,迭代周期很快。同時團隊也在不斷的擴張,對應拆分了組內不同的業務線對接不同業務線的需求,最初的Android客戶端單一的設計架構已經逐漸不滿足快速的業務開發需求。歷經組內討論開始對項目整理進行組件化的遷移,通過組件化的方式滿足不同業務線業務開發的穩定性,是迭代開發更靈活,組內協作開發效率得到提升。同時又有新的項目立項需要投入開發,一方面可以通過新項目實踐和推進組件化的遷移,另一方面也可以通過組件化拆分后的技術組件復用來更快的搭建和開發新的項目。
組件化的準備
技術準備
1. 主站最初的app項目只有一個模塊,業務耦合嚴重,技術組件很難復用,所以我們采取的第一步是拆分部分基礎組件下沉為一個Base庫,盡量去解耦業務提取基礎技術組件達到多業務模塊的復用,也是為了支持新項目和主站項目多個app的技術支持。
2. 考慮組件化后的業務相對隔離,但是客戶端組件間需要建立訪問,所以需要組件間通信的介入。我們采取的方式是路由、服務和全局通知。
3. 搭建路由庫支持,目的是解決業務組件物理隔離后的UI跳轉和訪問,通過維護路由表的方式尋址到需要訪問的業務組件UI。我們采取的是技術實現是通過注解給對應的業務UI比如LoginActivity上用注解申明對應的路由地址,在公共依賴的接口處公開維護這個路由地址常量,暴露給其他業務組件通過方位該地址來跳轉到對應的業務組件UI。
@Router(RouterPath.LOGIN_PAGE) public class LoginActivity extends BaseCompatActivitypublic class RouterPath {/*** 登錄*/public static final String LOGIN_PAGE = "/native/youpin-login.html";/*** 搜索key*/public static final String SEARCH_KEY = "/native/youpin-search-key.html";... }對應Act綁定上路由地址后,需要對路由的地址進行統一的收集管理。同時也為了支持某些服務動態下發的地址,策略是優先在本地的路由表進行匹配,如果查詢到了該地址有對應的Native界面優先跳轉到Native的界面,未匹配到則跳轉到由webView容器承載的網頁。目前我們采取的方式是通過APT自動生成對應路由注解后的activity的收集類。
//自動生成的類,命名規則是RouterGenerator+業務組件模塊名稱 //RouterGenerator_login.classpublic class RouterGenerator_login implements RouterProvider {public RouterGenerator_login() {}public void loadRouter(Map<String, Route> routerMap, Map<String, Route> pageNameRouterMap) {String keyLoginActivity = "((https|http|yiupin|native)://(w+.)?yiupin.com/native/youpin-login.html)|(" + RouteBuilder.generateUriFromClazz(LoginActivity.class) + ")";routerMap.put(keyLoginActivity, RouteBuilder.build(keyLoginActivity, 0, false, (String[])null, LoginActivity.class));} }然后再通過ASM的方式在編譯期對所有加載到工程里面的模塊組件通過特定的規則進行上面路由輔助類的收集。
//收集路由地址 ['scanInterface' : 'com.kaola.annotation.provider.RouterProvider','scanSuperClasses' : [],'codeInsertToClassName': 'com.kaola.core.center.router.RouterMap',//未指定codeInsertToMethodName,默認插入到static塊中,故此處register必須為static方法'registerMethodName' : 'register','include' : ['com/kaola/annotation/provider/result/.*' ]//根據工程依賴的所有組件模塊收集所有實現RouterProvider的輔助類。 //然后插入到RouterMap的靜態代碼塊中,默認調用無參構造。 //遍歷執行RouterMap中的靜態方法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);} }具體實現不再此展開了,此方式的好處就是可以根據需求加載需要的業務組件并且實現自動注冊和收集路由到路由表。如果覺得獨立開發路由庫的成本較高,也可以采取業界主流的一些路由庫比如ARouter等,基本類似。
4. 關于組件間服務通信的方式,目前采取的是暴露對應的服務接口供各個業務組件方調用。每個業務組件都會申明需要對外暴露提供的方法,并在自己的業務組件模塊內實現這些具體被調用的方法。對外接口庫根據模塊劃分,可以申明和維護通信間的一些數據類型,比如公開的數據model和對應需要訪問的一些路由地址等。為了便于服務的動態收集,這些服務接口可以統一的繼承某個規則接口,然后采取上述路由的方式,對所有實現了該規則接口的服務接口統一的收集管理。
facade/pay/model/PayModel.classIPayService.class//IService.class,統一對繼承IService的服務接口的具體實現類進行收集interface PayService : IService {fun startH5PaySercive(context: Context)}pay_module/PayServiceImpl.classclass PayServiceImpl : PayService {override fun startH5PaySercive(context: Context) {//...}}剩下一些特點場景的業務,比如:登錄成功后需要全局通知刷新多個UI某個業務狀態的時候,目前采取EventBus的方式進行訂閱通知。
5. 在組件base庫一定下沉和組件間通信方式的確立,開始對組件的具體的拆分粒度進行劃分。大致劃分為業務組件和技術組件兩部分。
組件化的拆分流程
拆分前的考慮
考慮新的項目投入的人力資源有限,并且需要快速的開發上線,同時業務也有重合的場景。所以當時采取的開發策略是將主站未組件化的代碼完全拷貝一份到新項目,并在此的基礎上進行改造。改造的原則必須遵循2個應用共建同一套BaseLib,但是由于主站的BaseLib里面會耦合一些自身的業務組件,同時避免對BaseLib的修改影響到主站的業務開發而增加不必要的工作量。當時采取的策略是通過增加一層業務基礎組件庫來做新項目組件化拆分的緩沖層BaseCompatLib。
拆分過程
拆分過程中有很多業務組件共用的情況,結合當時的開發周期可以適當的去解耦部分業務組件重新劃分到對應拆分后的業務模塊中。如果時間有限,可以先挪到BaseCompatLib這個緩沖成暫時共用待后續再拆,從而避免對2個項目共用的Base庫頻繁修改帶來的負擔。
初期的業務模塊獨立編譯的配置方式,僅供參考:
//gradle.properties中申明編譯配置是否是獨立編譯 # Module Build isModuleInjectBuild=true//moduleLibrary的build.gradle中申明編譯方式 if (isModuleInjectBuild.toBoolean()) {apply from: '../build_module.gradle' } else {apply from: '../build_app.gradle' }//新建一個appbuild文件,用來支業務組件以app方式編譯時所需的配置 //示例: java/appbuild/BuildInfo.class //獨立配置HomeServiceImpl.class //改寫應用啟動跳轉的UIApp.class //獨立編譯時的application,用于初始化配置android {//配置源碼路徑sourceSets {main {jniLibs.srcDirs = ['src/main/jnilibs']//如果是整體編譯,可以移除獨立編譯所需的額外代碼if (isModuleInjectBuild.toBoolean()) {java {exclude 'appbuild/**'}}}} }遇到的問題
拆分后的獨立模塊由于一些基礎服務的初始化仍停留在app殼工程,一些sdk或者初始化服務沒有統一的管理。優先級混亂并且耦合大量的業務邏輯,導致業務模塊拆分后無法獨立運行,缺失對應組件所需服務的初始化步驟。開始改造初始化的業務,原理同自動收集一致。
interface IInitializer {fun loadInQueue(queue: PriorityQueue<InitialTask>) //收集需要的服務進隊列fun init(processName: String) //對應初始化服務的實現 }class InitialManager {companion object {private val mInitializerQueue = PriorityQueue<InitialTask>() //服務隊列private var mCurProcessName: String = "" //當前啟動的進程//應用初始化時的調用的入口函數@JvmStaticfun initial(curProcessName: String) {mCurProcessName = curProcessNameinitialInProcess()}@JvmStaticfun initialInProcess() {loop@ while (mInitializerQueue.isNotEmpty()) { //搜索接入了多少三方sdk功能,總任務隊列val initialTask = mInitializerQueue.poll() //按優先級取//根據是否擁有權限去加載普通任務//特殊不需要檢查權限的任務,包括:Config和Permission初始化本身的任務。//目前這些優先級必須高于普通任務,否則會被提前打斷,等到權限獲取后才會執行。when {PermissionUtils.isNecessaryPermissionGranted() || initialTask.isNoNeedPermissionCheck() -> {executeTask(initialTask)}else -> {//一旦被權限檢查打斷不能執行,取出的任務重新放回隊列。跳出任務隊列,等待權限獲取后的再次執行。mInitializerQueue.add(initialTask)break@loop}}}}/*** 執行任務,匹配對應進程,對應進程啟動對應需要初始化的任務,沿用主站的邏輯*/private fun executeTask(initialTask: InitialTask) {initialTask.processName.forEach {//當前進程和服務需要初始化的進程相匹配或者是全進程需要就加載if (it == mCurProcessName || it == InitialTask.INITIAL_ALL_PROCESS) {Log.d("InitialManager", "initial - process:$mCurProcessName & initialTask:${initialTask.initialName}")initialTask.initializer.init(mCurProcessName)return@forEach}}}@JvmStaticfun register(initializer: IInitializer) {initializer.loadInQueue(mInitializerQueue)}}//示例服務 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()}} }徹底組件化
組件庫的獨立發布和維護
原有拆分的本地組件徹底分離出去,采取獨立發布和維護的方式迭代更新。
本地開發調試模式
在組件開發過程中,單純的依靠遠程方式依賴,對開發階段的頻繁修改不友好。所以我們采取依賴覆蓋的方式,讓原有的依賴在編譯過程中替換掉遠程的版本改用本地的版本進行引用。
// 自定義const.gradle環境聲明 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_sourcesdef 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的依賴從遠程替換為本地module的形式。開發階段就可以通過AS的refactor進行代碼的優化和重構,對本地Base修改后到Base的git分支進行對應的提交或MR合回主分支然后走規范的發布打包流程。
組件版本依賴管理
組件項目中會有對Base或者接口庫的引用,對于Base我們可以選擇compileOnly的方式,也可以選擇直接依賴的方式。在集成到項目中后依賴會遵循gradle的依賴傳遞原則。特別注意:
后續
到此為止基本上組件化就可以持續穩定的開發和維護了,組件化后也給團隊的開發效率帶來一定的提升,代碼也可以在一定可控的范圍內穩定的維護。并且在各自維護的組件中,大家也可以根據各自需求選擇合適自己業務的開發框架比如:mvp、LiveData、Rx等或者嘗試使用新語言Kotlin去編寫。解決業務耦合帶來的負擔同時也使各個組件達到了較高的可復用性,靈活的支持不同的應用項目,達到可插拔的方式集成開發。后續項目也會做一些優化,針對版本依賴的管理和簡化組件編譯和發布集成的流程來提高協作開發的效率。
ASM自動收集參考:https://github.com/luckybilly/AutoRegister
總結
以上是生活随笔為你收集整理的android app.build文件_网易友品 Android 客户端组件化演进的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 气温常年在25度的地方_最低调的海滨城市
- 下一篇: python 画图设置横纵坐标_Pyth