阿里P7移動互聯網架構師進階視頻(每日更新中)免費學習請點擊:https://space.bilibili.com/474380680
本篇文章將通過手寫組件化架構;路由框架原理與實現來闡述組件化框架設計
此次手寫架構,解決的問題是:
1、讓 App內 各個功能模塊能夠獨立開發單元測試,也可以 所有模塊集成打包,統一測試
獨立開發
更改gradle.properties的配置,使得每個功能模塊都成為application, 可以獨立打包成apk,單獨運行。單個模塊,獨立測試。
集成打包
更改gradle.properties的配置,使得原先每個單獨模塊,都變成library,被 主模塊引用,這時候只有主模塊能夠打包apk,所有功能都集成在這個apk內。
2、實現 功能模塊的整體移植,靈活拔插
故事背景
當你們公司有多個安卓開發人員,開發出核心業務相同,但是UI不同,其他業務不同的一系列App時(如果核心業務是X,你們有5個開發人員,做出了A,B,C,D,E 5個app,都包含核心業務X,但是除了X之外,其他的業務模塊各不相同)這時候,如果領導要把A里面的一個非核心功能,挪到B里面...
現狀
開發B的程序猿可能要罵娘,因為他在從移植A的代碼中剝離代碼 遇到了很多高耦合,低內聚 的類結構,挪過來之后,牽一發而動全身,動一點小地方,整個代碼滿江紅。
理想
如果這個時候,我們通過代碼框架的配置,能夠把A里面的一個模塊,作為一個module 移植到 工程內部,然后主module 來引用這個module,略微寫一些代碼來使得這個功能模塊在app中生效。那么無論是多少個功能模塊,都可以作為整體來 給其他app復用。這樣開發人員也不用相互罵娘了,如果挪過來的模塊存在bug或者其他問題,也不用甩鍋,模塊原本是誰開發的,找誰就好了。
3、保證App內 業務模塊的相互隔離,但是又不妨礙業務模塊之間的數據交互
我們開發app的功能模塊,一個業務,可能是通過一個Activity或者 一個Fragment 作為對外的窗口,也可能是。所謂窗口,就是這個業務,相對於其他模塊,"有且只有"一個入口,沒有任何其他可以觸達到這個業務的途徑。業務代碼之間相互隔離,絕對不可以有相互引用。那么,既然相互不會引用,那A模塊一定要用到B模塊的數據,怎么辦呢?下文提供解決方案。
正文大綱
1、代碼結構現狀以及理想狀態一覽
2、功能組件化的實現思路,實現組件移植拔插
3、參考ARouter源碼,寫出自己的Router框架,統一通過Router來進行模塊的切換 以及 組件之間數據的交互
4、使用組件api化,在模塊很多的情況下優化公共模塊的結構
正文
1、代碼結構現狀以及理想狀態一覽
現狀;
代碼有模塊化的跡象,但是沒有對業務模塊進行非常明顯的模塊化(不明白啥意思是吧?不明白就對了,app這個module里面其實還有很多東西沒有展示出來,請看下圖:試想,把所有的模塊集中到一個module的一個包里面,當你要移植某一個功能的時候,想想那酸爽....當然如果你口味別致,那當我沒說)
理想:
理想化的話,參照:理想.png; 項目結構層次分明,脈絡清晰
按照圖中的分層,詳細解釋一下:
外殼層:app module
內部代碼只寫 app的骨骼框架,比如說,你的app是這個樣子的結構:
下方有N個TAB,通過Fragment來進行切換模塊。這種架構肯定不少見。
這個時候,外殼層 app module,就只需要寫上 上面這種UI架構的框架代碼就行了,至於有多少個模塊,需要代碼去讀取配置進行顯示。你問我怎么寫這種UI框架嗎?網上一大把的,如果實在找不到,來我的 github項目地址
業務層
我們的業務模塊,對外接口可能是一個
Activity
* (比如說,登錄模塊,只對外提供一個LoginActivity
,有且僅有這一個窗口)或者 是一個Fragment
,就像上圖(典型的app架構.png), 如果app的UI框架是通過切換Fragment
來卻換業務模塊的話。用business
*這個目錄,將所有的業務模塊包含進去,每個模塊又是獨立的module
,這樣既實現了業務代碼隔離,又能一眼看到所有的業務模塊,正所謂,一目了然。
功能組件層
每一個業務模塊,不可避免的需要用到一些公用工具類,有的是第三方SDK的再次封裝,有的是自己的工具類,或者自己寫的自定義控件,還有可能是 所有業務模塊都需要的 輔助模塊,都放在這里。
路由框架層
設計這一層,是想讓app內的所有Activity,業務模塊Fragment,以及模塊之間的數據交互,都由 這一層開放出去的接口來負責
gradle統一配置文件
工程內部的一些全局gradle變量,放在這里,整個工程都有效
module編譯設置
setting.gradle 配置要編譯的module; 也可以做更復雜的操作,比如,寫gradle代碼去自動生成一些module,免除人為創建的麻煩.
2. 功能組件化的實現思路,實現組件移植拔插
能夠兼顧 每個模塊的單獨開發,單獨測試 和 整體打包,統一測試。 聽起來很神奇的樣子,但是其實就一個核心:gradle編程。
打開gradle.properties文件:
注解應該很清晰了,通過一個全局變量,就可以控制當前是要 模塊化單元測試呢?還是要集成打包apk測試。
那么,只寫一個isModule就完事了嗎?當然不是,還有一堆雜事 需要我們處理,我們要使用這個全局變量。
一堆雜事,分為兩類;
1- app 外殼層module 的build.gradle(注意:寫在dependencies)
2- 每個業務module的build.gradle
第一處:判定 isModule,決定當前module是要當成library還是application
第二處:更改defaultConfig里面的部分代碼,為什么要改?因為當當前module作為library的時候,不能有applicationId "XXXX"這一句
第三處:當業務模塊module作為library的時候,不可以在 AndroidManifest.xml中寫 Launcher Activity,否則,你打包app module的時候,安裝完畢,手機桌面上將會出現不止一個icon。而,當業務模塊module 作為application單獨運行的時候,必須有一個Launcher Activity ,不然...launcher都沒有,你測個什么鬼 ``` 所以這里針對manifest文件進行區分對待。
由於要區分對待,我們就需要另外創建一個manifest文件,移除launcher配置即可。參考下圖:
這就是業務模塊組件化的秘密了。
什么,你問我怎么 進行功能拔插嗎?
當你不需要某一個模塊的時候,
1)在app的build.gradle里面 把 引用該模塊的配置去掉;
2)setting.gradle 的include 去掉它
3)app module 里面,改動代碼,不再使用這個模塊。(這個我就不截圖了,因為app module的UI框架代碼不是一句話說得清的。請運行我的demo源碼自己看吧)
功能的插入,同理,上面的過程倒過來走一遍,就不浪費篇幅了。
3. 參考ARouter源碼,寫出自己的Router框架,統一通過Router來進行模塊的切換 以及組件之間數據的交互
說到路由框架的使用價值,有兩點:
1、在app實現了組件化之后,由於組件之間由於代碼隔離,不允許相互引用,導致 相互不能直接溝通,那么,就需要一個 “中間人角色” 來幫忙*" 帶話"了.
2、app內部,不可避免地要進行Activity跳轉,Fragment切換。把這些重復性的代碼,都統一讓路由來做吧。省了不少代碼行數。
閱讀了阿里巴巴ARouter的源碼,參照阿里大神的主要思路,簡化了一些流程,去掉了一些我不需要的功能,增加了一些我獨有的功能,加入了一些自己的想法,寫出了自己的 ZRouter 路由 框架。那就不羅嗦了,上干貨吧。
基礎知識
如果以下基礎知識不具備,建議先去學習基礎知識。 或者 也可以跟着筆者的思路來看代碼,慢慢理解這些知識的實用價值。
java反射機制(路由框架里大量地使用了 class反射創建 對象)
APT 注解,注解解析機制(注解解析機制貫穿了整個路由框架)
javapoet , java類的元素結構(一些人為寫起來很麻煩的代碼,一些臟活累活,就通過自動生成代碼來解決)
如何使用
1- 在app module的自定義Application類里面,進行初始化, ZRouter准備就緒
2- 就緒之后才可以直接使用(RouterPathConst 里面都是我自己定義的String常量):
切換Fragment
跳轉Activity
組件之間的通信,取得Mine模塊的 accountNo 然后 toast出來
如我們之前所設想的,切換Fragment,跳轉Activity,組件之間的通信 全部只能通過 ZRouter框架來執行。
3- 退出app時,要釋放ARouer的資源(主要是靜態變量)
4- 每個業務模塊,在將要暴露出去的Fragment或者Activity上,要加上注解
或者
或者
設計思路
講解設計思路,必須用源碼進行參照,請務必參照源碼 。
源碼地址為:https://github.com/18598925736/EvolutionPro
說明一下我本人 閱讀源碼的方法。也許很多人都和曾經的我一樣,看到一份第三方SDK的源碼,不知道從何下手,要么看了半天還在原地打轉轉,要么就是這里看一點,那里看一點,沒有中心思想,看了半天毫無收獲。
干貨:看源碼要思路清晰,目的明確。一切技術的價值都只有一個,那就是解決實際問題。既然是解決實際問題,那我們就從這個SDK暴露出來的最外圍接口為起點,看這個接口的作用是什么,解決了什么問題,順藤摸瓜,找找它解決問題的核心方法,至於順藤摸瓜道路上遇到的枝枝脈脈,要分清哪些是輔助類(每個人寫輔助類的習慣可能都不同,所以不必太在意),哪些是核心類(核心思想一般都是大同小異)。找到了核心思想,再從頭重新過幾遍,SDK的設計思路就了然於胸了哈.
按照我的上面提供的“干貨”,如果你現在下載了我的Demo源碼,那么我們繼續:
如果把看源碼的結構,理解為 警察查案。那么就要從最表層的現象開始着手,慢慢查找根源。
HomeFragment.java的54行, 這里要進行Activity跳轉。
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
這里有getInstance()方法,build()方法,還有navigation()方法,一個一個看
getInstance()
是處在ZRouter類內部,是ZRouter的單例模式的get方法,單例模式就不贅述了,我寫了注釋build()
方法也是在ZRouter類內部,邏輯很簡單,就是new Postcard(path)
參數path
是一個string
,方法返回值是一個Postcard
對象navigation()
方法是在Postcard類內部,但是,具體的執行邏輯,依然是在ZRouter
類里面getInstance()
和build()
方法都很簡單,不需要花太多精力。下面繼續跟隨ZRouter
的navigation()
方法“追查”
ZRouter
的 navigation()
方法內容如下:
發現一個可疑的代碼:LogisticsCenter.complete(postcard);
看方法名,應該是對postcard對象進行完善。
進去追查
這段代碼,從一個map
中,用path
作為key
,get
出了一個RouteMeat
對象,然后用這個對象的字段值,對參數postcard
的屬性進行賦值。好像有點莫名其妙。看不太懂。不着急,繼續。
剛才的navigation()
方法這里存在switch
分支,分支設計到ACTIVITY,FRAGMENT,PROVIDER
,由於我們這次追查的只是activity
相關,所以,忽略掉其他分支,只追查startActivity(postcard);
下面是該方法的代碼:
這里只是一個簡單的跳轉操作,但是,發現了一個關鍵點,跳轉的“目的地”class
是來自 postcard
的destination
. 發現規律了,原來剛才在 LogisticsCenter.complete(postcard);
里面進行postcard
“完善”的時候,set
進去的destination
原來在這里被使用到。
那么問題的關鍵點就發生了轉移了, 這個destination
Class
是從map
里面get
出來的,那么,又是什么時候被put
進去的呢?
開始追蹤這個
map
:Warehouse.routeMap
,通過代碼追蹤,可以發現,唯一可能往map
里面put
東西的代碼只有這一句:
利用java反射機制,反射創建類的實例,然后執行onLoad
方法,參數,正是這個map
OK,關於查看源碼的詳細步驟,就寫到這么多,再羅嗦,大佬們要打人啦。
目前為止的結論:通過追蹤ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
我們一路上遭遇了這些類或接口:
核心類 :
ZRouter(提供Activity跳轉的接口);
輔助類或接口
Postcard (“明信片”,封裝我們要執行操作,這次的操作是 跳Activity)
RouteMeta (“路由參數”,Postcard的基類)
RouteType (“路由類型”,我們要執行的操作,用枚舉來進行區分)
LogisticsCenter ("物流中心",主要封裝ZRouter類的一些特殊邏輯,比如對Postcard對象進行完善補充 )
Warehouse (“貨艙”,用hashMap來存儲“路由”對象)
IRouterZ ("路由注冊"接口類 ,用於反射創建對象,從而進行路由的注冊)
上面用大量篇幅詳述了 追蹤源碼, 追查框架結構的方法,那么下面的篇幅就直接說結論了:
路由框架的結構,可以用一張圖表示:
針對這張圖 簡單說兩句:
路由框架必然有3個部分,注解定義,注解解析,以及路由對外接口。
demo
中我把這3個部分定義成了3個module
.
其中,每個部分的核心代碼是:zrouter-annotation
模塊的ZRoute @interface,IRouterZ
接口zrouter-api
模塊的ZRouter
類zrouter-compiler
模塊的RouterProcessor
類
具體的代碼,不加以說明了。
如何用路由進行Activity
跳轉,我寫了詳細步驟,相信沒人看不懂了。那么Fragment
的切換,是我自定義的方法,可能有點粗糙,但是也是通俗易懂,就點到為止。但是,我們組件化的思想,就是要隔離所有的業務模塊,彼此之間不能進行直接通信,如果A模塊一定要使用B模塊的一些數據,通過路由框架也能實現。
HomeFragment類的第72行代碼:
String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo();
這句代碼的意義是:在Home模塊中,通過路由框架,調用Mine模塊對外開放的接口accountNo();
追蹤這句代碼的navigation()
方法,找到真正的執行邏輯 ZRouter類
141行起:
這里:最終返回了一個Provider對象.
LogisticsCenter
類又有了戲份:LogisticsCenter.buildProvider(serviceName)
和LogisticsCenter.complete(postcard);
分別點進去看:
buildProvider(String)
方法,其實就是從map
中找出RouteMeta
對象,然后返回一個Postcard
.
complete(Postcard)
方法,其實就是完善postcard
的字段,且,針對Provider
,進行特別處理,反射創建Provider
對象,並建立Provider
的緩存機制,防止多次進行數據交互時進行無意義的反射創建對象。
看到這里,整個路由框架,包括模塊間的通信,就講解完畢了。
做下結論吧:
使用路由框架的目的,是 在項目代碼組件化的背景之下,優化Activity跳轉,Fragment切換的重復代碼的編寫,而統一使用路由框架的對外接口執行跳轉或者切換。同時,通過路由框架的對外接口,實現組件之間的無障礙通信,保證組件的獨立性。
在探索框架的過程中,我們遇到了很多輔助類,但是輔助類怎么寫,完全看個人習慣,我是看了阿里巴巴的ARtouer框架之后得到啟發,按照它的思路來寫自己的路由框架,但是很多輔助類的寫法,我並完全按它的意思來。但是,核心思想,APT 注解+反射+自動生成代碼 是完全一樣的。
所以說,打蛇打七寸,看框架要看核心,拿住核心之后,其他的東西,就算代碼量再大,也是狐假虎威。
4、使用組件api化,在模塊很多的情況下優化公共模塊的結構
回顧一下理想中的項目結構:
背景
這里的功能組件層 function,是存放各個業務模塊都需要的公共類或者接口。這里說的公共類,也包含了剛才所提及的 業務模塊之間進行通信所需要的接口。
舉例說明:A模塊,需要調用B模塊的test()接口,由於A不能直接引用B模塊,那這個test接口,只能放在function這個公共模塊內,然后A,B同時引用,B對test接口進行實現並通過注解進行路由注冊,A通過路由對外接口調用B的test方法。
現狀
誠然,這種做法沒毛病,能夠實現功能。但是隨着項目模塊的增多,function 里面會存在很多的業務模塊數據接口。有一種情況:如果存在A,B,C,D,E 5個模塊,它們都在function內存放了 數據接口,並且5個模塊都引用了function模塊。那么,當A需要,並且只需要B的數據接口,而不需要C,D,E的接口時,它還是不得不去引用這些用不着的接口。A不需要這些接口,但是,還不得不引用!這顯然會不合邏輯。並且這種 全部業務數據接口都塞到function模塊里面的做法,會導致function出現不必要的臃腫。
理想
每個業務模塊的數據接口,只和本模塊的業務有關,所以最好是放在本模塊之內,但是,如果放在本模塊之內,又會導致組件之間不能通信. 那么就創建一個專門的 Module來存放每個業務模塊的接口。想法可行,但是每個業務模塊的module數量一下子加倍了,又會造成維護困難的問題。那么有沒有方法可以自動生成這些數據接口模塊呢? 還真有~ 神奇的gradle編程 >_<*
關鍵詞
組件API化技術 使用gradle配置,對module內的特殊后綴文件進行檢索,並以當前module為基礎,自動生成新的module.
上干貨:
這個名叫MineOpenServiceApi的接口,原本是.java后綴,現在改成.api
打開demo的setting.gradle文件:
找到下面的代碼:
這段代碼來自一位很厲害的大神,它的作用是,檢索指定模塊里面,有沒有指定后綴名(.api)的文件,有的話,找出來,經過一系列處理(注解很詳細,應該能看懂),自動生成一個module. 生成的module名字比原來的module多一個_api. 表示這個模塊,包含原模塊的所有對外數據接口
有幾處細節需要注意:
- 數據接口的.java后綴需要改成.api(整個.api完全和setting.gradle代碼里的.api對應,你可以都換成其他后綴,比如.apixxxxx)
- 原模塊里面,會多出一個api.gradle,這個文件的名字也和 setting.gradle里的api.gradle對應,也可以修改
這個api.gradle並不會在本模塊被編譯的時候起作用,但是它最終會變成 _api 新模塊的build.gradle,並保持完全一樣的代碼。 新的_api模塊只是一個library,所以,要去掉 本模塊里面的build.gradle里面針對isModule的判定。
OK,感受一下組件API化的成果:
理想實現了
現在不用把所有的數據接口都放到function公共模塊內,而只需要在本模塊之內將數據接口文件后綴改成.api,然后在setting.gradle里面使用自定義的方法進行include。 就可以只引用本模塊需要的 數據接口module,而不需要引用多余的module,而且,防止了function模塊的無意義的膨脹。簡直破費。
原文鏈接:https://www.jianshu.com/p/e73bc515ab53
阿里P7移動互聯網架構師進階視頻(每日更新中)免費學習請點擊:https://space.bilibili.com/474380680