前言
手把手講解系列文章呵萨,是我寫給各位看官溺蕉,也是寫給我自己的伶丐。
文章可能過(guò)分詳細(xì),但是這是為了幫助到盡量多的人疯特,畢竟工作5,6年哗魂,不能老吸血,也到了回饋開(kāi)源的時(shí)候.
這個(gè)系列的文章:
1漓雅、用通俗易懂的講解方式录别,講解一門技術(shù)的實(shí)用價(jià)值
2、詳細(xì)書(shū)寫源碼的追蹤邻吞,源碼截圖组题,繪制類的結(jié)構(gòu)圖,盡量詳細(xì)地解釋原理的探索過(guò)程
3抱冷、提供Github 的 可運(yùn)行的Demo工程,但是我所提供代碼崔列,更多是提供思路,拋磚引玉旺遮,請(qǐng)酌情cv
4赵讯、集合整理原理探索過(guò)程中的一些坑,或者demo的運(yùn)行過(guò)程中的注意事項(xiàng)
5耿眉、用gif圖边翼,最直觀地展示demo運(yùn)行效果如果覺(jué)得細(xì)節(jié)太細(xì),直接跳過(guò)看結(jié)論即可鸣剪。
本人能力有限讯私,如若發(fā)現(xiàn)描述不當(dāng)之處,歡迎留言批評(píng)指正西傀。
學(xué)到老活到老斤寇,路漫漫其修遠(yuǎn)兮。與眾君共勉 !
引子
最近得高人指點(diǎn)拥褂,恰巧工作中做過(guò)一個(gè)移植其他app的某個(gè)功能模塊的任務(wù)娘锁,過(guò)程簡(jiǎn)直痛不欲生。
然后思考如何對(duì)app的各個(gè)功能模塊進(jìn)行靈活拔插配置饺鹃,最大程度減少移植代碼出錯(cuò)的可能性莫秆,保證功能模塊的完整移植
此次手寫架構(gòu),解決的問(wèn)題是:
1悔详、讓 App內(nèi) 各個(gè)功能模塊能夠獨(dú)立開(kāi)發(fā)單元測(cè)試镊屎,也可以 所有模塊集成打包,統(tǒng)一測(cè)試
獨(dú)立開(kāi)發(fā)
更改gradle.properties的配置茄螃,使得每個(gè)功能模塊都成為application缝驳, 可以獨(dú)立打包成apk,單獨(dú)運(yùn)行。單個(gè)模塊用狱,獨(dú)立測(cè)試运怖。
集成打包
更改gradle.properties的配置,使得原先每個(gè)單獨(dú)模塊夏伊,都變成library摇展,被 主模塊引用,這時(shí)候只有主模塊能夠打包apk溺忧,所有功能都集成在這個(gè)apk內(nèi)咏连。
2、實(shí)現(xiàn) 功能模塊的整體移植鲁森,靈活拔插
故事背景
當(dāng)你們公司有多個(gè)安卓開(kāi)發(fā)人員捻勉,開(kāi)發(fā)出核心業(yè)務(wù)相同,但是UI不同刀森,其他業(yè)務(wù)不同的一系列App時(shí)(如果核心業(yè)務(wù)是X,你們有5個(gè)開(kāi)發(fā)人員报账,做出了A,B,C,D,E 5個(gè)app研底,都包含核心業(yè)務(wù)X,但是除了X之外透罢,其他的業(yè)務(wù)模塊各不相同)這時(shí)候榜晦,如果領(lǐng)導(dǎo)要把A里面的一個(gè)非核心功能,挪到B里面...
現(xiàn)狀
開(kāi)發(fā)B的程序猿可能要罵娘羽圃,因?yàn)樗趶囊浦睞的代碼中剝離代碼 遇到了很多高耦合乾胶,低內(nèi)聚 的類結(jié)構(gòu),挪過(guò)來(lái)之后朽寞,牽一發(fā)而動(dòng)全身识窿,動(dòng)一點(diǎn)小地方,整個(gè)代碼滿江紅脑融。
理想
如果這個(gè)時(shí)候喻频,我們通過(guò)代碼框架的配置,能夠把A里面的一個(gè)模塊肘迎,作為一個(gè)module 移植到 工程內(nèi)部甥温,然后主module 來(lái)引用這個(gè)module,略微寫一些代碼來(lái)使得這個(gè)功能模塊在app中生效妓布。那么無(wú)論是多少個(gè)功能模塊姻蚓,都可以作為整體來(lái) 給其他app復(fù)用。這樣開(kāi)發(fā)人員也不用相互罵娘了匣沼,如果挪過(guò)來(lái)的模塊存在bug或者其他問(wèn)題狰挡,也不用甩鍋,模塊原本是誰(shuí)開(kāi)發(fā)的,找誰(shuí)就好了圆兵。
3跺讯、保證App內(nèi) 業(yè)務(wù)模塊的相互隔離,但是又不妨礙業(yè)務(wù)模塊之間的數(shù)據(jù)交互
我們開(kāi)發(fā)app的功能模塊殉农,一個(gè)業(yè)務(wù)刀脏,可能是通過(guò)一個(gè)Activity或者 一個(gè)Fragment 作為對(duì)外的窗口,也可能是。所謂窗口超凳,就是這個(gè)業(yè)務(wù)愈污,相對(duì)于其他模塊,"有且只有"一個(gè)入口轮傍,沒(méi)有任何其他可以觸達(dá)到這個(gè)業(yè)務(wù)的途徑暂雹。業(yè)務(wù)代碼之間相互隔離,絕對(duì)不可以有相互引用创夜。那么杭跪,既然相互不會(huì)引用,那A模塊一定要用到B模塊的數(shù)據(jù)驰吓,怎么辦呢涧尿?下文提供解決方案。
鳴謝
感謝享學(xué)課堂的老師們的公開(kāi)課 https://ke.qq.com/course/341933
感謝群里兄弟們的旁敲側(cè)擊
感謝阿里巴巴大佬們的ARouter開(kāi)源框架
正文大綱
1檬贰、代碼結(jié)構(gòu)現(xiàn)狀以及理想狀態(tài)一覽
2姑廉、功能組件化的實(shí)現(xiàn)思路,實(shí)現(xiàn)組件移植拔插
3翁涤、參考ARouter源碼桥言,寫出自己的Router框架,統(tǒng)一通過(guò)Router來(lái)進(jìn)行模塊的切換 以及 組件之間數(shù)據(jù)的交互
4葵礼、使用組件api化号阿,在模塊很多的情況下優(yōu)化公共模塊的結(jié)構(gòu)
正文
1、代碼結(jié)構(gòu)現(xiàn)狀以及理想狀態(tài)一覽
先來(lái)看兩張圖
現(xiàn)狀
代碼有模塊化的跡象鸳粉,但是沒(méi)有對(duì)業(yè)務(wù)模塊進(jìn)行非常明顯的模塊化(不明白啥意思是吧倦西?不明白就對(duì)了,app這個(gè)module里面其實(shí)還有很多東西沒(méi)有展示出來(lái)赁严,請(qǐng)看下圖:試想扰柠,把所有的模塊集中到一個(gè)module的一個(gè)包里面,當(dāng)你要移植某一個(gè)功能的時(shí)候疼约,想想那酸爽....當(dāng)然如果你口味別致卤档,那當(dāng)我沒(méi)說(shuō))
理想:
理想化的話,參照:理想.png; 項(xiàng)目結(jié)構(gòu)層次分明程剥,脈絡(luò)清晰
按照?qǐng)D中的分層劝枣,詳細(xì)解釋一下:
外殼層:app module
內(nèi)部代碼只寫 app的骨骼框架汤踏,比如說(shuō),你的app是這個(gè)樣子的結(jié)構(gòu):
下方有N個(gè)TAB舔腾,通過(guò)Fragment來(lái)進(jìn)行切換模塊溪胶。這種架構(gòu)肯定不少見(jiàn)。
這個(gè)時(shí)候稳诚,外殼層 app module哗脖,就只需要寫上 上面這種UI架構(gòu)的框架代碼就行了,至于有多少個(gè)模塊扳还,需要代碼去讀取配置進(jìn)行顯示才避。神馬?你問(wèn)我怎么寫這種UI框架氨距?網(wǎng)上一大把桑逝,如果實(shí)在找不到,來(lái)我的 github項(xiàng)目地址
?乛?乛?
業(yè)務(wù)層
我們的業(yè)務(wù)模塊俏让,對(duì)外接口可能是一個(gè)
Activity
(比如說(shuō)楞遏,登錄模塊,只對(duì)外提供一個(gè)LoginActivity
,有且僅有這一個(gè)窗口)或者 是一個(gè)Fragment
首昔,就像上圖(典型的app架構(gòu).png), 如果app的UI框架是通過(guò)切換Fragment
來(lái)卻換業(yè)務(wù)模塊的話寡喝。用business
這個(gè)目錄,將所有的業(yè)務(wù)模塊包含進(jìn)去沙廉,每個(gè)模塊又是獨(dú)立的module
,這樣既實(shí)現(xiàn)了業(yè)務(wù)代碼隔離臼节,又能一眼看到所有的業(yè)務(wù)模塊撬陵,正所謂,一目了然网缝。
功能組件層
每一個(gè)業(yè)務(wù)模塊巨税,不可避免的需要用到一些公用工具類,有的是第三方SDK的再次封裝粉臊,有的是自己的工具類草添,或者自己寫的自定義控件,還有可能是 所有業(yè)務(wù)模塊都需要的 輔助模塊扼仲,都放在這里远寸。
路由框架層
設(shè)計(jì)這一層,是想讓app內(nèi)的所有Activity屠凶,業(yè)務(wù)模塊Fragment驰后,以及模塊之間的數(shù)據(jù)交互,都由 這一層開(kāi)放出去的接口來(lái)負(fù)責(zé)
gradle統(tǒng)一配置文件
工程內(nèi)部的一些全局gradle變量矗愧,放在這里灶芝,整個(gè)工程都有效
module編譯設(shè)置
setting.gradle 配置要編譯的module; 也可以做更復(fù)雜的操作,比如,寫gradle代碼去自動(dòng)生成一些module夜涕,免除人為創(chuàng)建的麻煩.
2. 功能組件化的實(shí)現(xiàn)思路犯犁,實(shí)現(xiàn)組件移植拔插
能夠兼顧 每個(gè)模塊的單獨(dú)開(kāi)發(fā),單獨(dú)測(cè)試 和 整體打包女器,統(tǒng)一測(cè)試酸役。 聽(tīng)起來(lái)很神奇的樣子,但是其實(shí)就一個(gè)核心:gradle編程晓避。
打開(kāi)gradle.properties文件:
注解應(yīng)該很清晰了簇捍,通過(guò)一個(gè)全局變量,就可以控制當(dāng)前是要 模塊化單元測(cè)試呢俏拱?還是要集成打包apk測(cè)試暑塑。
那么,只寫一個(gè)isModule就完事了嗎锅必?當(dāng)然不是事格,還有一堆雜事 需要我們處理,我們要使用這個(gè)全局變量搞隐。
一堆雜事驹愚,分為兩類:
1- app 外殼層module 的build.gradle(注意:寫在dependencies)
if (isModule.toBoolean()) {
implementation project(":business:activity_XXX")
//...在這里引用更多業(yè)務(wù)模塊
}
2- 每個(gè)業(yè)務(wù)module的build.gradle
第一處:判定 isModule,決定當(dāng)前module是要當(dāng)成library還是application
if (isModule.toBoolean()) {
apply plugin:'com.android.library'
} else {
apply plugin:'com.android.application'*
}
第二處:更改defaultConfig里面的部分代碼劣纲,為什么要改逢捺?因?yàn)楫?dāng)當(dāng)前module作為library的時(shí)候,不能有applicationId "XXXX"這一句
defaultConfig {
if (!isModule.toBoolean()) {
applicationId"study.hank.com.XXXX"*
}
....
}
第三處:當(dāng)業(yè)務(wù)模塊module作為library的時(shí)候癞季,不可以在 AndroidManifest.xml中寫 Launcher Activity,否則劫瞳,你打包app module的時(shí)候,安裝完畢绷柒,手機(jī)桌面上將會(huì)出現(xiàn)不止一個(gè)icon志于。而,當(dāng)業(yè)務(wù)模塊module 作為application單獨(dú)運(yùn)行的時(shí)候废睦,必須有一個(gè)Launcher Activity 伺绽,不然...launcher都沒(méi)有,你測(cè)個(gè)球 ``` 所以這里針對(duì)manifest文件進(jìn)行區(qū)分對(duì)待嗜湃。
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
由于要區(qū)分對(duì)待奈应,我們就需要另外創(chuàng)建一個(gè)manifest文件,移除launcher配置即可购披。參考下圖:
這就是業(yè)務(wù)模塊組件化的秘密了钥组。
什么,你問(wèn)我怎么 進(jìn)行功能拔插今瀑?
當(dāng)你不需要某一個(gè)模塊的時(shí)候程梦,
1)在app的build.gradle里面 把 引用該模塊的配置去掉点把;
2)setting.gradle 的include 去掉它
3)app module 里面,改動(dòng)代碼屿附,不再使用這個(gè)模塊郎逃。(這個(gè)我就不截圖了,因?yàn)閍pp module的UI框架代碼不是一句話說(shuō)得清的挺份。請(qǐng)運(yùn)行我的demo源碼自己看吧)
功能的插入褒翰,同理,上面的過(guò)程倒過(guò)來(lái)走一遍匀泊,就不浪費(fèi)篇幅了优训。
3. 參考ARouter源碼,寫出自己的Router框架各聘,統(tǒng)一通過(guò)Router來(lái)進(jìn)行模塊的切換 以及組件之間數(shù)據(jù)的交互
說(shuō)到路由框架的使用價(jià)值揣非,兩點(diǎn):
1、在app實(shí)現(xiàn)了組件化之后躲因,由于組件之間由于代碼隔離早敬,不允許相互引用,導(dǎo)致 相互不能直接溝通大脉,那么搞监,就需要一個(gè) “中間人角色” 來(lái)幫忙" 帶話"了.
2、app內(nèi)部镰矿,不可避免地要進(jìn)行Activity跳轉(zhuǎn)琐驴,F(xiàn)ragment切換。把這些重復(fù)性的代碼秤标,都統(tǒng)一讓路由來(lái)做吧绝淡。省了不少代碼行數(shù)。
閱讀了阿里巴巴ARouter的源碼抛杨,參照阿里大神的主要思路够委,簡(jiǎn)化了一些流程荐类,去掉了一些我不需要的功能怖现,增加了一些我獨(dú)有的功能,加入了一些自己的想法玉罐,寫出了自己的 ZRouter 路由 框架屈嗤。那就不羅嗦了,上干貨吊输。
基礎(chǔ)知識(shí)
如果以下基礎(chǔ)知識(shí)不具備饶号,建議先去學(xué)習(xí)基礎(chǔ)知識(shí)。 或者 也可以跟著筆者的思路來(lái)看代碼季蚂,慢慢理解這些知識(shí)的實(shí)用價(jià)值。
java反射機(jī)制(路由框架里大量地使用了 class反射創(chuàng)建 對(duì)象)
APT 注解刽肠,注解解析機(jī)制(注解解析機(jī)制貫穿了整個(gè)路由框架)
javapoet 诈嘿, java類的元素結(jié)構(gòu)(一些人為寫起來(lái)很麻煩的代碼,一些臟活累活涩禀,就通過(guò)自動(dòng)生成代碼來(lái)解決)
如何使用
1- 在app module的自定義Application類里面,進(jìn)行初始化然眼, ZRouter準(zhǔn)備就緒
public class FTApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ZRouter.getInstance().initRegister(this);
}
}
2- 就緒之后才可以直接使用(RouterPathConst 里面都是我自己定義的String常量**):
切換Fragment
ZRouter.getInstance().build(RouterPathConst.PATH_FRAGMENT_MINE).navigation();
跳轉(zhuǎn)Activity
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
組件之間的通信,取得Mine模塊的 accountNo 然后 toast出來(lái)
String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo();
Toast.makeText(getActivity(), accountNo, Toast.LENGTH_LONG).show();
如我們之前所設(shè)想的艾船,切換Fragment,跳轉(zhuǎn)Activity高每,組件之間的通信 全部只能通過(guò) ZRouter框架來(lái)執(zhí)行屿岂。
3- 退出app時(shí),要釋放ARouer的資源(主要是靜態(tài)變量)
ZRouter.getInstance().release();
4- 每個(gè)業(yè)務(wù)模塊鲸匿,在將要暴露出去的Fragment或者Activity上爷怀,要加上注解
@ZRoute(RouterPathConst.PATH_ACTIVITY_CHART)//注冊(cè)Activity
public class ChartActivity extends AppCompatActivity {···}
或者
@ZRoute(RouterPathConst.PATH_FRAGMENT_HOME)//注冊(cè)Fragment
public class HomeFragment extends Fragment {···}
或者
@ZRoute(RouterPathConst.PATH_PROVIDER_MINE) // 注冊(cè)數(shù)據(jù)接口
public class MineServiceImpl implements MineOpenServiceApi {···}
設(shè)計(jì)思路
講解設(shè)計(jì)思路,必須用源碼進(jìn)行參照晒骇,請(qǐng)務(wù)必參照源碼 霉撵。
源碼地址為:https://github.com/18598925736/EvolutionPro
說(shuō)明一下本人 閱讀源碼的方法。也許很多人都和曾經(jīng)的我一樣洪囤,看到一份第三方SDK的源碼徒坡,不知道從何下手,要么看了半天還在原地打轉(zhuǎn)轉(zhuǎn)瘤缩,要么就是這里看一點(diǎn)喇完,那里看一點(diǎn),沒(méi)有中心思想剥啤,看了半天毫無(wú)收獲锦溪。
干貨:看源碼要思路清晰,目的明確府怯。一切技術(shù)的價(jià)值都只有一個(gè)刻诊,那就是解決實(shí)際問(wèn)題。既然是解決實(shí)際問(wèn)題牺丙,那我們就從這個(gè)SDK暴露出來(lái)的最外圍接口為起點(diǎn)则涯,看這個(gè)接口的作用是什么,解決了什么問(wèn)題冲簿,順藤摸瓜粟判,找找它解決問(wèn)題的核心方法,至于順藤摸瓜道路上遇到的枝枝脈脈峦剔,要分清哪些是輔助類(每個(gè)人寫輔助類的習(xí)慣可能都不同档礁,所以不必太在意),哪些是核心類(核心思想一般都是大同小異)吝沫。找到了核心思想呻澜,再?gòu)念^重新過(guò)幾遍递礼,SDK的設(shè)計(jì)思路就了然于胸了.
按照我的上面提供的“干貨”,如果你現(xiàn)在下載了我的Demo源碼羹幸,那么我們繼續(xù):
如果把看源碼的結(jié)構(gòu)宰衙,理解為 警察查案。那么就要從最表層的現(xiàn)象開(kāi)始著手睹欲,慢慢查找根源供炼。
HomeFragment.java的54行, 這里要進(jìn)行Activity跳轉(zhuǎn)窘疮。
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
這里有g(shù)etInstance()方法袋哼,build()方法,還有navigation()方法,一個(gè)一個(gè)看
getInstance()
是處在ZRouter類內(nèi)部闸衫,是ZRouter的單例模式的get方法涛贯,單例模式就不贅述了,我寫了注釋build()
方法也是在ZRouter類內(nèi)部蔚出,邏輯很簡(jiǎn)單弟翘,就是new Postcard(path)
參數(shù)path
是一個(gè)string
,方法返回值是一個(gè)Postcard
對(duì)象navigation()
方法是在Postcard類內(nèi)部,但是骄酗,具體的執(zhí)行邏輯稀余,依然是在ZRouter
類里面
getInstance()
和build()
方法都很簡(jiǎn)單,不需要花太多精力趋翻。下面繼續(xù)跟隨ZRouter
的navigation()
方法“追查”
ZRouter
的navigation()
方法內(nèi)容如下:
Object navigation(Postcard postcard) {
LogisticsCenter.complete(postcard);
switch (postcard.getRouteType()) {
case ACTIVITY://如果是Activity睛琳,那就跳吧
return startActivity(postcard);
case FRAGMENT://如果是Fragment,那就切換吧
return switchFragment(postcard);
case PROVIDER://如果是Provider踏烙,那就執(zhí)行業(yè)務(wù)邏輯
return postcard.getProvider();//那就直接返回provider對(duì)象
default:
break;
}
return null;
}
發(fā)現(xiàn)一個(gè)可疑的代碼:
LogisticsCenter.complete(postcard);
看方法名师骗,應(yīng)該是對(duì)postcard對(duì)象進(jìn)行完善。
進(jìn)去追查
/**
* Postcard字段補(bǔ)全
*
* @param postcard
*/
public static void complete(Postcard postcard) {
if (null == postcard) {
throw new RuntimeException("err:postcard 是空的讨惩,怎么搞的辟癌?");
}
RouteMeta routeMeta = Warehouse.routeMap.get(postcard.getPath());//
if (null == routeMeta) {//如果路由meta是空,說(shuō)明可能這個(gè)路由沒(méi)注冊(cè)荐捻,也有可能路由表沒(méi)有去加載到內(nèi)存中
throw new RuntimeException("err:路由尋址失敗黍少,請(qǐng)檢查是否path寫錯(cuò)了");
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setRouteType(routeMeta.getRouteType());
···
}
}
這段代碼,從一個(gè)
map
中靴患,用path
作為key
仍侥,get
出了一個(gè)RouteMeat
對(duì)象要出,然后用這個(gè)對(duì)象的字段值鸳君,對(duì)參數(shù)postcard
的屬性進(jìn)行賦值。好像有點(diǎn)莫名其妙患蹂』蚣眨看不太懂砸紊。不著急,繼續(xù)囱挑。
剛才的
navigation()
方法這里存在switch
分支醉顽,分支設(shè)計(jì)到ACTIVITY,FRAGMENT,PROVIDER
,由于我們這次追查的只是activity
相關(guān)平挑,所以游添,忽略掉其他分支,只追查startActivity(postcard)通熄;
下面是該方法的代碼:
private Object startActivity(Postcard postcard) {
Class<?> cls = postcard.getDestination();
if (cls == null) {
if (cls == null)
throw new RuntimeException("沒(méi)找到對(duì)應(yīng)的activity唆涝,請(qǐng)檢查路由尋址標(biāo)識(shí)是否寫錯(cuò)");
}
final Intent intent = new Intent(mContext, cls);
if (Postcard.FLAG_DEFAULT != postcard.getFlag()) {//如果不是初始值,也就是說(shuō)唇辨,flag值被更改過(guò)廊酣,那就用更改后的值
intent.setFlags(postcard.getFlag());
} else {//如果沒(méi)有設(shè)定啟動(dòng)模式,即 flag值沒(méi)有被更改赏枚,就用常規(guī)模式啟動(dòng)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//常規(guī)模式啟動(dòng)Activity
}
//跳轉(zhuǎn)只能在主線程中進(jìn)行
runInMainThread(new Runnable() {
@Override
public void run() {
mContext.startActivity(intent);
}
});
return null;
}
這里只是一個(gè)簡(jiǎn)單的跳轉(zhuǎn)操作亡驰,但是,發(fā)現(xiàn)了一個(gè)關(guān)鍵點(diǎn)饿幅,跳轉(zhuǎn)的“目的地”
class
是來(lái)自postcard
的destination
. 發(fā)現(xiàn)規(guī)律了凡辱,原來(lái)剛才在LogisticsCenter.complete(postcard);
里面進(jìn)行postcard
“完善”的時(shí)候,set
進(jìn)去的destination
原來(lái)在這里被使用到栗恩。
那么問(wèn)題的關(guān)鍵點(diǎn)就發(fā)生了轉(zhuǎn)移了煞茫, 這個(gè)
destination
Class
是從map
里面get
出來(lái)的,那么摄凡,又是什么時(shí)候被put
進(jìn)去的呢续徽?
開(kāi)始追蹤這個(gè)map
:Warehouse.routeMap
,通過(guò)代碼追蹤亲澡,可以發(fā)現(xiàn)钦扭,唯一可能往map
里面put
東西的代碼只有這一句:
/**
* 反射執(zhí)行APT注冊(cè)文件的注冊(cè)方法
*/
private static void registerComm() {
try {
Set<String> classNames = ClassUtils.getFileNameByPackageName(mContext, RouterConst.GENERATION_PACKAGE_NAME);//找到包名下的所有class
for (String className : classNames) {
Class<?> clz = Class.forName(className);
if (IRouterZ.class.isAssignableFrom(clz)) {
IRouterZ iRouterComm = (IRouterZ) clz.getConstructor().newInstance();
iRouterComm.onLoad(Warehouse.routeMap);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
Warehouse.traversalCommMap();
}
}
利用java反射機(jī)制,反射創(chuàng)建類的實(shí)例床绪,然后執(zhí)行
onLoad
方法客情,參數(shù),正是這個(gè)map
OK,關(guān)于查看源碼的詳細(xì)步驟癞己,就寫到這么多膀斋,再羅嗦,大佬們要打人啦痹雅。
目前為止的結(jié)論:通過(guò)追蹤
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
我們一路上遭遇了這些類或接口:
核心類 :
ZRouter(提供Activity跳轉(zhuǎn)的接口);輔助類或接口
Postcard (“明信片”仰担,封裝我們要執(zhí)行操作,這次的操作是 跳Activity)
RouteMeta (“路由參數(shù)”,Postcard的基類)
RouteType *(“路由類型”绩社,我們要執(zhí)行的操作摔蓝,用枚舉來(lái)進(jìn)行區(qū)分) *
LogisticsCenter ("物流中心"赂苗,主要封裝ZRouter類的一些特殊邏輯,比如對(duì)Postcard對(duì)象進(jìn)行完善補(bǔ)充 )
Warehouse (“貨艙”贮尉,用hashMap來(lái)存儲(chǔ)“路由”對(duì)象)
IRouterZ ("路由注冊(cè)"接口類 拌滋,用于反射創(chuàng)建對(duì)象,從而進(jìn)行路由的注冊(cè))
上面用大量篇幅詳述了 追蹤源碼猜谚, 追查框架結(jié)構(gòu)的方法败砂,那么下面的篇幅就直接說(shuō)結(jié)論了:
路由框架的結(jié)構(gòu),可以用一張圖表示:
針對(duì)這張圖 簡(jiǎn)單說(shuō)兩句:
路由框架必然有3個(gè)部分魏铅,注解定義吠卷,注解解析,以及路由對(duì)外接口沦零。
demo
中我把這3個(gè)部分定義成了3個(gè)module
.
其中祭隔,每個(gè)部分的核心代碼是:
zrouter-annotation
模塊的ZRoute @interface,IRouterZ
接口
zrouter-api
模塊的ZRouter
類
zrouter-compiler
模塊的RouterProcessor
類
具體的代碼路操,不加以說(shuō)明了疾渴。
如何用路由進(jìn)行Activity
跳轉(zhuǎn),我寫了詳細(xì)步驟屯仗,相信沒(méi)人看不懂了搞坝。那么Fragment
的切換,是我自定義的方法魁袜,可能有點(diǎn)粗糙桩撮,但是也是通俗易懂,就點(diǎn)到為止峰弹。但是店量,我們組件化的思想,就是要隔離所有的業(yè)務(wù)模塊鞠呈,彼此之間不能進(jìn)行直接通信融师,如果A模塊一定要使用B模塊的一些數(shù)據(jù),通過(guò)路由框架也能實(shí)現(xiàn)蚁吝。
HomeFragment類的第72行代碼:
String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo();
這句代碼的意義是:在Home模塊中旱爆,通過(guò)路由框架,調(diào)用Mine模塊對(duì)外開(kāi)放的接口accountNo();
追蹤這句代碼的navigation()
方法窘茁,找到真正的執(zhí)行邏輯 ZRouter類
141行起:
public <T> T navigation(String serviceName) {
Postcard postcard = LogisticsCenter.buildProvider(serviceName);
if (null == postcard)
return null;
LogisticsCenter.complete(postcard);//補(bǔ)全postcard字段值
return (T) postcard.getProvider();
}
這里:最終返回了一個(gè)Provider對(duì)象.
LogisticsCenter
類又有了戲份:LogisticsCenter.buildProvider(serviceName)
和LogisticsCenter.complete(postcard);
分別點(diǎn)進(jìn)去看:
buildProvider(String)
方法怀伦,其實(shí)就是從map
中找出RouteMeta
對(duì)象,然后返回一個(gè)Postcard
.
public static Postcard buildProvider(String name) {
RouteMeta routeMeta = Warehouse.routeMap.get(name);
if (null == routeMeta) {
return null;
} else {
return new Postcard(routeMeta.getPath());
}
}
complete(Postcard)
方法山林,其實(shí)就是完善postcard
的字段房待,且,針對(duì)Provider
,進(jìn)行特別處理吴攒,反射創(chuàng)建Provider
對(duì)象,并建立Provider
的緩存機(jī)制砂蔽,防止多次進(jìn)行數(shù)據(jù)交互時(shí)進(jìn)行無(wú)意義的反射創(chuàng)建對(duì)象洼怔。
/**
* Postcard字段補(bǔ)全
*
* @param postcard
*/
public static void complete(Postcard postcard) {
if (null == postcard) {
throw new RuntimeException("err:postcard 是空的,怎么搞的左驾?");
}
RouteMeta routeMeta = Warehouse.routeMap.get(postcard.getPath());//
if (null == routeMeta) {//如果路由meta是空镣隶,說(shuō)明可能這個(gè)路由沒(méi)注冊(cè),也有可能路由表沒(méi)有去加載到內(nèi)存中
throw new RuntimeException("err:路由尋址失敗诡右,請(qǐng)檢查是否path寫錯(cuò)了");
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setRouteType(routeMeta.getRouteType());
switch (routeMeta.getRouteType()) {
case PROVIDER://如果是數(shù)據(jù)接口Provider的話
Class<? extends IProvider> clz = (Class<? extends IProvider>) routeMeta.getDestination();
//從map中找找看
IProvider provider = Warehouse.providerMap.get(clz);
//如果沒(méi)找到
if (null == provider) {
//執(zhí)行反射方法創(chuàng)建安岂,并且存入到map
try {
provider = clz.getConstructor().newInstance();
provider.init(mContext);
Warehouse.providerMap.put(clz, provider);
} catch (Exception e) {
e.printStackTrace();
}
}
postcard.setProvider(provider);
break;
default:
break;
}
}
}
看到這里,整個(gè)路由框架帆吻,包括模塊間的通信域那,就講解完畢了。
做個(gè)結(jié)論:
使用路由框架的目的猜煮,是 在項(xiàng)目代碼組件化的背景之下次员,優(yōu)化Activity跳轉(zhuǎn),F(xiàn)ragment切換的重復(fù)代碼的編寫王带,而統(tǒng)一使用路由框架的對(duì)外接口執(zhí)行跳轉(zhuǎn)或者切換淑蔚。同時(shí),通過(guò)路由框架的對(duì)外接口愕撰,實(shí)現(xiàn)組件之間的無(wú)障礙通信刹衫,保證組件的獨(dú)立性。
在探索框架的過(guò)程中搞挣,我們遇到了很多輔助類带迟,但是輔助類怎么寫,完全看個(gè)人習(xí)慣囱桨,我是看了阿里巴巴的ARtouer框架之后得到啟發(fā)邮旷,按照它的思路來(lái)寫自己的路由框架,但是很多輔助類的寫法蝇摸,我并完全按它的意思來(lái)婶肩。但是,核心思想貌夕,APT 注解+反射+自動(dòng)生成代碼 是完全一樣的律歼。
所以說(shuō),打蛇打七寸啡专,看框架要看核心险毁,拿住核心之后,其他的東西,就算代碼量再大畔况,也是狐假虎威鲸鹦。
4、使用組件api化跷跪,在模塊很多的情況下優(yōu)化公共模塊的結(jié)構(gòu)
回顧一下理想中的項(xiàng)目結(jié)構(gòu):
背景
這里的功能組件層 function馋嗜,是存放各個(gè)業(yè)務(wù)模塊都需要的公共類或者接口。這里說(shuō)的公共類吵瞻,也包含了剛才所提及的 業(yè)務(wù)模塊之間進(jìn)行通信所需要的接口葛菇。
舉例說(shuō)明:A模塊,需要調(diào)用B模塊的test()接口橡羞,由于A不能直接引用B模塊眯停,那這個(gè)test接口,只能放在function這個(gè)公共模塊內(nèi)卿泽,然后A,B同時(shí)引用莺债,B對(duì)test接口進(jìn)行實(shí)現(xiàn)并通過(guò)注解進(jìn)行路由注冊(cè),A通過(guò)路由對(duì)外接口調(diào)用B的test方法签夭。
現(xiàn)狀
誠(chéng)然九府,這種做法沒(méi)毛病,能夠?qū)崿F(xiàn)功能覆致。但是隨著項(xiàng)目模塊的增多侄旬,function 里面會(huì)存在很多的業(yè)務(wù)模塊數(shù)據(jù)接口。有一種情況:如果存在A,B,C,D,E 5個(gè)模塊煌妈,它們都在function內(nèi)存放了 數(shù)據(jù)接口儡羔,并且5個(gè)模塊都引用了function模塊。那么璧诵,當(dāng)A需要汰蜘,并且只需要B的數(shù)據(jù)接口,而不需要C,D,E的接口時(shí)之宿,它還是不得不去引用這些用不著的接口族操。A不需要這些接口,但是比被,還不得不引用色难!這顯然會(huì)不合邏輯。并且這種 全部業(yè)務(wù)數(shù)據(jù)接口都塞到function模塊里面的做法等缀,會(huì)導(dǎo)致function出現(xiàn)不必要的臃腫枷莉。
理想
每個(gè)業(yè)務(wù)模塊的數(shù)據(jù)接口,只和本模塊的業(yè)務(wù)有關(guān)尺迂,所以最好是放在本模塊之內(nèi)笤妙,但是冒掌,如果放在本模塊之內(nèi),又會(huì)導(dǎo)致組件之間不能通信. 那么就創(chuàng)建一個(gè)專門的 Module來(lái)存放每個(gè)業(yè)務(wù)模塊的接口蹲盘。想法可行股毫,但是每個(gè)業(yè)務(wù)模塊的module數(shù)量一下子加倍了,又會(huì)造成維護(hù)困難的問(wèn)題召衔。那么有沒(méi)有方法可以自動(dòng)生成這些數(shù)據(jù)接口模塊呢铃诬? 還真有~ 神奇的gradle編程 >_<
關(guān)鍵詞
組件API化技術(shù) 使用gradle配置,對(duì)module內(nèi)的特殊后綴文件進(jìn)行檢索薄嫡,并以當(dāng)前module為基礎(chǔ)氧急,自動(dòng)生成新的module.
不羅嗦颗胡,直接上干貨:
這個(gè)名叫MineOpenServiceApi的接口毫深,原本是.java后綴,現(xiàn)在改成.api
打開(kāi)demo的setting.gradle文件:
找到下面的代碼:
include_with_api(':business:fragment_mine')
def include_with_api(String moduleName) {
include(moduleName)
//獲得工程根目錄
String originDir = project(moduleName).projectDir
//制作的 SDK 工程的目錄
String targetDir = "${originDir}_api"
//制作的 SDK 工程的名字
String sdkName = "${project(moduleName).name}_api"
System.out.println("-------------------------------------SDK name:" + sdkName)
//刪除掉 SDK 工程目錄 除了 iml
FileTree targetFiles = fileTree(targetDir)
targetFiles.exclude "*.iml"
targetFiles.each { File file ->
file.delete()
}
//從待制作SDK工程拷貝目錄到 SDK工程 只拷貝目錄
copy {
from originDir
into targetDir
//拷貝文件
include '**/*.api'
include '**/AndroidManifest.xml'
include 'api.gradle'
}
//讀取實(shí)現(xiàn)模塊的manifest并將package的值后加 .api 作為API工程的manifest package
FileTree manifests = fileTree(targetDir).include("**/AndroidManifest.xml")
manifests.each {
File file ->
def parser = new XmlParser().parse(file)
def node = parser.attribute('package')
parser.attributes().replace('package', "${node}.api")
new XmlNodePrinter(new PrintWriter(file)).print(parser)
}
//將api.gradle改為build.gradle
File build = new File(targetDir + "/api.gradle")
if (build.exists()) {
build.renameTo(new File(targetDir + "/build.gradle"))
}
// 將.api 文件改為 .java
FileTree files = fileTree(targetDir).include("**/*.api")
files.each {
File file ->
file.renameTo(new File(file.absolutePath.replace(".api", ".java")))
}
//加入 SDK工程
include ":business:" + "$sdkName"
}
這段代碼來(lái)自一位”真“大神毒姨,它的作用是哑蔫,檢索指定模塊里面,有沒(méi)有指定后綴名(.api)的文件弧呐,有的話闸迷,找出來(lái),經(jīng)過(guò)一系列處理(注解很詳細(xì)俘枫,應(yīng)該能看懂)腥沽,自動(dòng)生成一個(gè)module. 生成的module名字比原來(lái)的module多一個(gè)_api. 表示這個(gè)模塊,包含原模塊的所有對(duì)外數(shù)據(jù)接口
有幾處細(xì)節(jié)需要注意:
- 數(shù)據(jù)接口的.java后綴需要改成.api(整個(gè).api完全和setting.gradle代碼里的.api對(duì)應(yīng)鸠蚪,你可以都換成其他后綴今阳,比如.apixxxxx)
- 原模塊里面,會(huì)多出一個(gè)api.gradle,這個(gè)文件的名字也和 setting.gradle里的api.gradle對(duì)應(yīng)茅信,也可以修改
這個(gè)api.gradle并不會(huì)在本模塊被編譯的時(shí)候起作用盾舌,但是它最終會(huì)變成 _api 新模塊的build.gradle,并保持完全一樣的代碼蘸鲸。 新的_api模塊只是一個(gè)library妖谴,所以,要去掉 本模塊里面的build.gradle里面針對(duì)isModule的判定酌摇。
OK,感受一下組件API化的成果:
理想實(shí)現(xiàn)了
現(xiàn)在不用把所有的數(shù)據(jù)接口都放到function公共模塊內(nèi)膝舅,而只需要在本模塊之內(nèi)將數(shù)據(jù)接口文件后綴改成.api,然后在setting.gradle里面使用自定義的方法進(jìn)行include窑多。 就可以只引用本模塊需要的 數(shù)據(jù)接口module铸史,而不需要引用多余的module,而且,防止了function模塊的無(wú)意義的膨脹怯伊。簡(jiǎn)直破費(fèi)琳轿。
結(jié)語(yǔ)
組件化的全攻略+Demo 已經(jīng)全部放送完畢判沟。
特別說(shuō)明:Demo只是提供一種組件化的全攻略,可能demo的代碼并沒(méi)有十分完善崭篡,比如:原ARouter源碼內(nèi)的帶參數(shù)的跳轉(zhuǎn)挪哄,或者startActivityForResult,由于時(shí)間關(guān)系我都去除了琉闪。一些輔助向的設(shè)計(jì)思路迹炼,我也并沒(méi)有完全遵照ARouter源碼。
這篇博客的技術(shù)來(lái)自 享學(xué)課堂的公開(kāi)課颠毙,建議大家可以去聽(tīng)聽(tīng) https://ke.qq.com/course/341933
如果有大佬看了文章之后有問(wèn)題斯入,或者意見(jiàn)建議,歡迎留言討論蛀蜜。 >_<!