一文帶你全面掌握Android組件化核心!
大家好,我是劉望舒,騰訊最具價(jià)值專家,著有三本業(yè)內(nèi)知名暢銷書,連續(xù)五年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,百度百科收錄的資深技術(shù)專家。
前華為面試官、獨(dú)角獸公司技術(shù)總監(jiān)。
BAT?即可。作者:看書的小蝸牛?
https://www.jianshu.com/p/e7bbe365ebc1
前端開發(fā)經(jīng)常遇到一個(gè)詞:路由,在Android APP開發(fā)中,路由還經(jīng)常和組件化開發(fā)強(qiáng)關(guān)聯(lián)在一起,那么到底什么是路由,一個(gè)路由框架到底應(yīng)該具備什么功能,實(shí)現(xiàn)原理是什么樣的?路由是否是APP的強(qiáng)需求呢?與組件化到底什么關(guān)系,本文就簡(jiǎn)單分析下如上幾個(gè)問題。
1.路由的概念
路由這個(gè)詞本身應(yīng)該是互聯(lián)網(wǎng)協(xié)議中的一個(gè)詞,維基百科對(duì)此的解釋如下:
路由(routing)就是通過互聯(lián)的網(wǎng)絡(luò)把信息從源地址傳輸?shù)侥康牡刂返幕顒?dòng)。路由發(fā)生在OSI網(wǎng)絡(luò)參考模型中的第三層即網(wǎng)絡(luò)層。
個(gè)人理解,在前端開發(fā)中,路由就是通過一串字符串映射到對(duì)應(yīng)業(yè)務(wù)的能力。APP的路由框首先能夠搜集各組件的路由scheme,并生成路由表,然后,能夠根據(jù)外部輸入字符串在路由表中匹配到對(duì)應(yīng)的頁(yè)面或者服務(wù),進(jìn)行跳轉(zhuǎn)或者調(diào)用,并提供會(huì)獲取返回值等,示意如下:

所以一個(gè)基本路由框架要具備如下能力:
1. APP路由的掃描及注冊(cè)邏輯。
2. ?路由跳轉(zhuǎn)target頁(yè)面能力。
3. 路由調(diào)用target服務(wù)能力。
APP中,在進(jìn)行頁(yè)面路由的時(shí)候,經(jīng)常需要判斷是否登錄等一些額外鑒權(quán)邏輯所以,還需要提供攔截邏輯等,比如:登陸。
2.三方路由框架是否是APP強(qiáng)需求
答案:不是,系統(tǒng)原生提供路由能力,但功能較少,稍微大規(guī)模的APP都采用三方路由框架。
Android系統(tǒng)本身提供頁(yè)面跳轉(zhuǎn)能力:如startActivity,對(duì)于工具類APP,或單機(jī)類APP,這種方式已經(jīng)完全夠用,完全不需要專門的路由框架,那為什么很多APP還是采用路由框架呢?這跟APP性質(zhì)及路由框架的優(yōu)點(diǎn)都有關(guān)。比如淘寶、京東、美團(tuán)等這些大型APP,無論是從APP功能還是從其研發(fā)團(tuán)隊(duì)的規(guī)模上來說都很龐大,不同的業(yè)務(wù)之間也經(jīng)常是不同的團(tuán)隊(duì)在維護(hù),采用組件化的開發(fā)方式,最終集成到一個(gè)APK中。
多團(tuán)隊(duì)之間經(jīng)常會(huì)涉及業(yè)務(wù)間的交互,比如從電影票業(yè)務(wù)跳轉(zhuǎn)到美食業(yè)務(wù),但是兩個(gè)業(yè)務(wù)是兩個(gè)獨(dú)立的研發(fā)團(tuán)隊(duì),代碼實(shí)現(xiàn)上是完全隔離的,那如何進(jìn)行通信呢?首先想到的是代碼上引入,但是這樣會(huì)打破了低耦合的初衷,可能還會(huì)引入各種問題。
例如,部分業(yè)務(wù)是外包團(tuán)隊(duì)來做,這就牽扯到代碼安全問題,所以還是希望通過一種類似黑盒的方式,調(diào)用目標(biāo)業(yè)務(wù),這就需要中轉(zhuǎn)路由支持,所以國(guó)內(nèi)很多APP都是用了路由框架的。其次我們各種跳轉(zhuǎn)的規(guī)則并不想跟具體的實(shí)現(xiàn)類扯上關(guān)系,比如跳轉(zhuǎn)商詳?shù)臅r(shí)候,不希望知道是哪個(gè)Activity來實(shí)現(xiàn),只需要一個(gè)字符串映射過去即可,這對(duì)于H5、或者后端開發(fā)來處理跳轉(zhuǎn)的時(shí)候,就非常標(biāo)準(zhǔn)。
3.原生路由的限制:功能單一,擴(kuò)展靈活性差,不易協(xié)同
傳統(tǒng)的路由基本上就限定在startActivity、或者startService來路由跳轉(zhuǎn)或者啟動(dòng)服務(wù)。拿startActivity來說,傳統(tǒng)的路由有什么缺點(diǎn):startActivity有兩種用法,一種是顯示的,一種是隱式的,顯示調(diào)用如下:
import?com.snail.activityforresultexample.test.SecondActivity;
public?class?MainActivity?extends?AppCompatActivity?{
????void?jumpSecondActivityUseClassName(){
????
????????Intent?intent?=new?Intent(MainActivity.this,?SecondActivity.class);
????????startActivity(intent);
????}
顯示調(diào)用的缺點(diǎn)很明顯,那就是必須要強(qiáng)依賴目標(biāo)Activity的類實(shí)現(xiàn),有些場(chǎng)景,尤其是大型APP組件化開發(fā)時(shí)候,有些業(yè)務(wù)邏輯出于安全考慮,并不想被源碼或aar依賴,這時(shí)顯式依賴的方式就無法走通。再來看看隱式調(diào)用方法。
第一步:manifest中配置activity的intent-filter,至少要配置一個(gè)action。
<manifest?xmlns:android="http://schemas.android.com/apk/res/android"
????package="com.snail.activityforresultexample">
????<application
???????...
????<activity?android:name=".test.SecondActivity">
????????????<intent-filter>
????????????
???????????????????<category?android:name="android.intent.category.DEFAULT"/>
????????????
????????????????<action?android:name="com.snail.activityforresultexample.SecondActivity"?/>
????????????????
??
????????????intent-filter>
????????activity>
????application>
manifest>
第二步:調(diào)用。
void?jumpSecondActivityUseFilter()?{
????Intent?intent?=?new?Intent();
????intent.setAction("com.snail.activityforresultexample.SecondActivity");
????startActivity(intent);
}如果牽扯到數(shù)據(jù)傳遞寫法上會(huì)更復(fù)雜一些,隱式調(diào)用的缺點(diǎn)有如下幾點(diǎn):
首先manifest中定義復(fù)雜,相對(duì)應(yīng)的會(huì)導(dǎo)致暴露的協(xié)議變的復(fù)雜,不易維護(hù)擴(kuò)展。
其次,不同Activity都要不同的action配置,每次增減修改Activity都會(huì)很麻煩,對(duì)比開發(fā)者非常不友好,增加了協(xié)作難度。
最后,Activity的export屬性并不建議都設(shè)置成True,這是降低風(fēng)險(xiǎn)的一種方式,一般都是收歸到一個(gè)Activity,DeeplinkActivitiy統(tǒng)一處理跳轉(zhuǎn),這種場(chǎng)景下,DeeplinkActivitiy就兼具路由功能,隱式調(diào)用的場(chǎng)景下,新Activitiy的增減勢(shì)必每次都要調(diào)整路由表,這會(huì)導(dǎo)致開發(fā)效率降低,風(fēng)險(xiǎn)增加。
可以看到系統(tǒng)原生的路由框架,并沒太多考慮團(tuán)隊(duì)協(xié)同的開發(fā)模式,多限定在一個(gè)模塊內(nèi)部多個(gè)業(yè)務(wù)間直接相互引用,基本都要代碼級(jí)依賴,對(duì)于代碼及業(yè)務(wù)隔離很不友好。如不考慮之前Dex方法樹超限制,可以認(rèn)為三方路由框架完全是為了團(tuán)隊(duì)協(xié)同而創(chuàng)建的。
4.APP三方路由框架需具備的能力
目前市面上大部分的路由框架都能搞定上述問題,簡(jiǎn)單整理下現(xiàn)在三方路由的能力,可歸納如下:
路由表生成能力:業(yè)務(wù)組件[UI業(yè)務(wù)及服務(wù)]自動(dòng)掃描及注冊(cè)邏輯,需要擴(kuò)展性好,無需入侵原有代碼邏輯。
scheme與業(yè)務(wù)映射邏輯 :無需依賴具體實(shí)現(xiàn),做到代碼隔離。
基礎(chǔ)路由跳轉(zhuǎn)能力 :頁(yè)面跳轉(zhuǎn)能力的支持。
服務(wù)類組件的支持 :如去某個(gè)服務(wù)組件獲取一些配置等。
[擴(kuò)展]路由攔截邏輯:比如登陸,統(tǒng)一鑒權(quán)。
可定制的降級(jí)邏輯:找不到組件時(shí)的兜底。
可以看下一個(gè)典型的Arouter用法,第一步:對(duì)新增頁(yè)面添加Router Scheme 聲明。
? ?
@Route(path?=?"/test/activity2")
public?class?Test2Activity?extends?AppCompatActivity?{
?????...
}build階段會(huì)根據(jù)注解搜集路由scheme,生成路由表。第二步使用:
ARouter.getInstance()
????????.build("/test/activity2")
????????.navigation(this);
如上,在ARouter框架下,僅需要字符串scheme,無需依賴任何Test2Activity就可實(shí)現(xiàn)路由跳轉(zhuǎn)。
5.APP路由框架的實(shí)現(xiàn)
路由框架實(shí)現(xiàn)的核心是建立scheme和組件[Activity或者其他服務(wù)]的映射關(guān)系,也就是路由表,并能根據(jù)路由表路由到對(duì)應(yīng)組件的能力。其實(shí)分兩部分,第一部分路由表的生成,第二部分,路由表的查詢。
路由表的自動(dòng)生成
生成路由表的方式有很多,最簡(jiǎn)單的就是維護(hù)一個(gè)公共文件或者類,里面映射好每個(gè)實(shí)現(xiàn)組件跟scheme。

不過,這種做法缺點(diǎn)很明顯:每次增刪修改都要都要修改這個(gè)表,對(duì)于協(xié)同非常不友好,不符合解決協(xié)同問題的初衷。不過,最終的路由表倒是都是這條路,就是將所有的Scheme搜集到一個(gè)對(duì)象中,只是實(shí)現(xiàn)方式的差別,目前幾乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面編程)來實(shí)現(xiàn)的,基本流程如下:

其中牽扯的技術(shù)有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面編程)。APT常用的有JavaPoet,主要是遍歷所有類,找到被注解的Java類,然后聚合生成路由表,由于組件可能有很多,路由表可能也有也有多個(gè),之后,這些生成的輔助類會(huì)跟源碼一并被編譯成class文件,之后利用AOP技術(shù)【如ASM或者JavaAssist】,掃描這些生成的class,聚合路由表,并填充到之前的占位方法中,完成自動(dòng)注冊(cè)的邏輯。
JavaPoet如何搜集并生成路由表集合?
以ARouter框架為例,先定義Router框架需要的注解如:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public?@interface?Route?{
????/**
?????*?Path?of?route
?????*/
????String?path();
該注解用于標(biāo)注需要路由的組件,用法如下:
@Route(path?=?"/test/activity1",?name?=?"測(cè)試用?Activity")
public?class?Test1Activity?extends?BaseActivity?{
????@Autowired
????int?age?=?10;
之后利用APT掃描所有被注解的類,生成路由表,實(shí)現(xiàn)參考如下:
@Override
public?boolean?process(Set?extends?TypeElement>?annotations,?RoundEnvironment?roundEnv)?{
????if?(CollectionUtils.isNotEmpty(annotations))?{
????
????????Set?extends?Element>?routeElements?=?roundEnv.getElementsAnnotatedWith(Route.class);
????????
????????????this.parseRoutes(routeElements);
???????...
????return?false;
}
?
private?void?parseRoutes(Set?extends?Element>?routeElements)?throws?IOException?{
????????????????????????...
?????????????????????//?Generate?groups
????????????String?groupFileName?=?NAME_OF_GROUP?+?groupName;
????????????JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
????????????????????TypeSpec.classBuilder(groupFileName)
????????????????????????????.addJavadoc(WARNING_TIPS)
????????????????????????????.addSuperinterface(ClassName.get(type_IRouteGroup))
????????????????????????????.addModifiers(PUBLIC)
????????????????????????????.addMethod(loadIntoMethodOfGroupBuilder.build())
????????????????????????????.build()
????????????).build().writeTo(mFiler);
產(chǎn)物如下:包含路由表,及局部注冊(cè)入口。

自動(dòng)注冊(cè):ASM搜集上述路由表并聚合插入Init代碼區(qū)。
為了能夠插入到Init代碼區(qū),首先需要預(yù)留一個(gè)位置,一般定義一個(gè)空函數(shù),以待后續(xù)填充:
? ?
public?class?RouterInitializer?{
????public?static?void?init(boolean?debug,?Class?webActivityClass,?IRouterInterceptor...?interceptors)?{
????????...
????????loadRouterTables();
????}
????//自動(dòng)注冊(cè)代碼????
????public?static?void?loadRouterTables()?{
????}
}首先利用AOP工具,遍歷上述APT中間產(chǎn)物,聚合路由表,并注冊(cè)到預(yù)留初始化位置,遍歷的過程牽扯是gradle transform的過程。
搜集目標(biāo),聚合路由表
/**掃描jar*/
fun?scanJar(jarFile:?File,?dest:?File?)?{
????val?file?=?JarFile(jarFile)
????var?enumeration?=?file.entries()
????while?(enumeration.hasMoreElements())?{
????????val?jarEntry?=?enumeration.nextElement()
????????if?(jarEntry.name.endsWith("XXRouterTable.class"))?{
????????????val?inputStream?=?file.getInputStream(jarEntry)
????????????val?classReader?=?ClassReader(inputStream)
????????????if?(Arrays.toString(classReader.interfaces)
????????????????????.contains("IHTRouterTBCollect")
????????????)?{
????????????????tableList.add(
????????????????????Pair(
????????????????????????classReader.className,
????????????????????????dest?.absolutePath
????????????????????)
????????????????)
????????????}
????????????inputStream.close()
????????}?else?if?(jarEntry.name.endsWith("HTRouterInitializer.class"))?{
????????????registerInitClass?=?dest
????????}
????}
????file.close()
}
對(duì)目標(biāo)Class注入路由表初始化代碼
fun?asmInsertMethod(originFile:?File?)?{
????val?optJar?=?File(originFile?.parent,?originFile?.name?+?".opt")
????if?(optJar.exists())
????????optJar.delete()
????val?jarFile?=?JarFile(originFile)
????val?enumeration?=?jarFile.entries()
????val?jarOutputStream?=?JarOutputStream(FileOutputStream(optJar))
????while?(enumeration.hasMoreElements())?{
????????val?jarEntry?=?enumeration.nextElement()
????????val?entryName?=?jarEntry.getName()
????????val?zipEntry?=?ZipEntry(entryName)
????????val?inputStream?=?jarFile.getInputStream(jarEntry)
????????//插樁class
????????if?(entryName.endsWith("RouterInitializer.class"))?{
????????????//class文件處理
????????????jarOutputStream.putNextEntry(zipEntry)
????????????val?classReader?=?ClassReader(IOUtils.toByteArray(inputStream))
????????????val?classWriter?=?ClassWriter(classReader,?ClassWriter.COMPUTE_MAXS)
????????????val?cv?=?RegisterClassVisitor(Opcodes.ASM5,?classWriter,tableList)
????????????classReader.accept(cv,?EXPAND_FRAMES)
????????????val?code?=?classWriter.toByteArray()
????????????jarOutputStream.write(code)
????????}?else?{
????????????jarOutputStream.putNextEntry(zipEntry)
????????????jarOutputStream.write(IOUtils.toByteArray(inputStream))
????????}
????????jarOutputStream.closeEntry()
????}
????//結(jié)束
????jarOutputStream.close()
????jarFile.close()
????if?(originFile?.exists()?==?true)?{
????????Files.delete(originFile.toPath())
????}
????optJar.renameTo(originFile)
}最終RouterInitializer.class的 loadRouterTables會(huì)被修改成如下填充好的代碼:
public?static?void?loadRouterTables()?{
??
??register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
??register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
??register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
??register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
??...
}
如此就完成了路由表的搜集與注冊(cè),大概的流程就是如此。當(dāng)然對(duì)于支持服務(wù)、Fragment等略有不同,但大體類似。
Router框架對(duì)服務(wù)類組件的支持
通過路由的方式獲取服務(wù)屬于APP路由比較獨(dú)特的能力,比如有個(gè)用戶中心的組件,我們可以通過路由的方式去查詢用戶是否處于登陸狀態(tài),這種就不是狹義上的頁(yè)面路由的概念,通過一串字符串如何查到對(duì)應(yīng)的組件并調(diào)用其方法呢?這種的實(shí)現(xiàn)方式也有多種,每種實(shí)現(xiàn)方式都有自己的優(yōu)劣。
一種是可以將服務(wù)抽象成接口,沉到底層,上層實(shí)現(xiàn)通過路由方式映射對(duì)象。
一種是將實(shí)現(xiàn)方法直接通過路由方式映射。
先看第一種,這種事Arouter的實(shí)現(xiàn)方式,它的優(yōu)點(diǎn)是所有對(duì)外暴露的服務(wù)都暴露接口類【沉到底層】,這對(duì)于外部的調(diào)用方,也就是服務(wù)使用方非常友好,示例如下:
先定義抽象服務(wù),并沉到底層

public?interface?HelloService?extends?IProvider?{
????void?sayHello(String?name);
}
實(shí)現(xiàn)服務(wù),并通過Router注解標(biāo)記。
@Route(path?=?"/yourservicegroupname/hello")
public?class?HelloServiceImpl?implements?HelloService?{
????Context?mContext;
????@Override
????public?void?sayHello(String?name)?{
????????Toast.makeText(mContext,?"Hello?"?+?name,?Toast.LENGTH_SHORT).show();
????}使用:利用Router加scheme獲取服務(wù)實(shí)例,并映射成抽象類,然后直接調(diào)用方法。
((HelloService)?ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");
這種實(shí)現(xiàn)方式對(duì)于使用方其實(shí)是很方便的,尤其是一個(gè)服務(wù)有多個(gè)可操作方法的時(shí)候,但是缺點(diǎn)是擴(kuò)展性,如果想要擴(kuò)展方法,就要改動(dòng)底層庫(kù)。
再看第二種:將實(shí)現(xiàn)方法直接通過路由方式映射
服務(wù)的調(diào)用都要落到方法上,參考頁(yè)面路由,也可以支持方法路由,兩者并列關(guān)系,所以主要增加一個(gè)方法路由表,實(shí)現(xiàn)原理與Page路由類似,跟上面的Arouter對(duì)比,不用定義抽象層,直接定義實(shí)現(xiàn)即可:
定義Method的Router
public?class?HelloService?{
????
????@MethodRouter(url?=?{"arouter://sayhello"})
????public?void?sayHello(String?name)?{
????????Toast.makeText(mContext,?"Hello?"?+?name,?Toast.LENGTH_SHORT).show();
????}
使用即可
RouterCall.callMethod("arouter://sayhello?name=hello");
上述的缺點(diǎn)就是對(duì)于外部調(diào)用有些復(fù)雜,尤其是處理參數(shù)的時(shí)候,需要嚴(yán)格按照協(xié)議來處理,優(yōu)點(diǎn)是,沒有抽象層,如果需要擴(kuò)展服務(wù)方法,不需要改動(dòng)底層。
上述兩種方式各有優(yōu)劣,不過,如果從做服務(wù)組件的初衷出發(fā),第一種比較好:對(duì)于調(diào)用方比較友好。另外對(duì)于CallBack的支持,Arouter的處理方式可能也會(huì)更方便一些,可以比較方便的交給服務(wù)方定義。如果是第二種,服務(wù)直接通過路由映射的方式,處理起來就比較麻煩,尤其是Callback中的參數(shù),可能要統(tǒng)一封裝成JSON并維護(hù)解析的協(xié)議,這樣處理起來,可能不是很好。
路由表的匹配
路由表的匹配比較簡(jiǎn)單,就是在全局Map中根據(jù)String輸入,匹配到目標(biāo)組件,然后依賴反射等常用操作,定位到目標(biāo)。
6.組件化與路由的關(guān)系
組件化是一種開發(fā)集成模式,更像一種開發(fā)規(guī)范,更多是為團(tuán)隊(duì)協(xié)同開發(fā)帶來方便。組件化最終落地是一個(gè)個(gè)獨(dú)立的業(yè)務(wù)及功能組件,這些組件之間可能是不同的團(tuán)隊(duì),處于不同的目的在各自維護(hù),甚至是需要代碼隔離,如果牽扯到組件間的調(diào)用與通信,就不可避免的借助路由,因?yàn)閷?shí)現(xiàn)隔離的,只能采用通用字符串scheme進(jìn)行通信,這就是路由的功能范疇。
組件化需要路由支撐的根本原因:組件間代碼實(shí)現(xiàn)的隔離。
總結(jié)
路由不是一個(gè)APP的必備功能,但是大型跨團(tuán)隊(duì)的APP基本都需要。
路由框架的基本能力:路由自動(dòng)注冊(cè)、路由表搜集、服務(wù)及UI界面路由及攔截等核心功能。
組件化與路由的關(guān)系:組件化的代碼隔離導(dǎo)致路由框架成為必須。

? 耗時(shí)2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!
為了防止失聯(lián),歡迎關(guān)注我的小號(hào)
??微信改了推送機(jī)制,真愛請(qǐng)星標(biāo)本公號(hào)??
