一文帶你全面掌握Android元件化核心!
BATcoder技術 群,讓一部分人先進大廠
大家好,我是劉望舒,騰訊最具價值專家,著有三本業內知名暢銷書,連續五年蟬聯電子工業出版社年度優秀作者,百度百科收錄的資深技術專家。
前華為面試官、獨角獸公司技術總監。
想要 加入 BATcoder技術群,公號回覆 BAT
即可。
作者:看書的小蝸牛
http://www.jianshu.com/p/e7bbe365ebc1
前端開發經常遇到一個詞:路由,在Android APP開發中,路由還經常和元件化開發強關聯在一起,那麼到底什麼是路由,一個路由框架到底應該具備什麼功能,實現原理是什麼樣的?路由是否是APP的強需求呢?與元件化到底什麼關係,本文就簡單分析下如上幾個問題。
1.路由的概念
路由這個詞本身應該是網際網路協議中的一個詞,維基百科對此的解釋如下:
路由(routing)就是通過互聯的網路把資訊從源地址傳輸到目的地址的活動。路由發生在OSI網路參考模型中的第三層即網路層。
個人理解,在前端開發中,路由就是通過一串字串對映到對應業務的能力。APP的路由框首先能夠蒐集各元件的路由scheme,並生成路由表,然後,能夠根據外部輸入字串在路由表中匹配到對應的頁面或者服務,進行跳轉或者呼叫,並提供會獲取返回值等,示意如下:
所以一個基本路由框架要具備如下能力:
1. APP路由的掃描及註冊邏輯。
2. 路由跳轉target頁面能力。
3. 路由呼叫target服務能力。
APP中,在進行頁面路由的時候,經常需要判斷是否登入等一些額外鑑權邏輯所以,還需要提供攔截邏輯等,比如:登陸。
2.三方路由框架是否是APP強需求
答案:不是,系統原生提供路由能力,但功能較少,稍微大規模的APP都採用三方路由框架。
Android系統本身提供頁面跳轉能力:如startActivity,對於工具類APP,或單機類APP,這種方式已經完全夠用,完全不需要專門的路由框架,那為什麼很多APP還是採用路由框架呢?這跟APP性質及路由框架的優點都有關。比如淘寶、京東、美團等這些大型APP,無論是從APP功能還是從其研發團隊的規模上來說都很龐大,不同的業務之間也經常是不同的團隊在維護,採用元件化的開發方式,最終整合到一個APK中。
多團隊之間經常會涉及業務間的互動,比如從電影票業務跳轉到美食業務,但是兩個業務是兩個獨立的研發團隊,程式碼實現上是完全隔離的,那如何進行通訊呢?首先想到的是程式碼上引入,但是這樣會打破了低耦合的初衷,可能還會引入各種問題。
例如,部分業務是外包團隊來做,這就牽扯到程式碼安全問題,所以還是希望通過一種類似黑盒的方式,呼叫目標業務,這就需要中轉路由支援,所以國內很多APP都是用了路由框架的。其次我們各種跳轉的規則並不想跟具體的實現類扯上關係,比如跳轉商詳的時候,不希望知道是哪個Activity來實現,只需要一個字串對映過去即可,這對於H5、或者後端開發來處理跳轉的時候,就非常標準。
3.原生路由的限制:功能單一,擴充套件靈活性差,不易協同
傳統的路由基本上就限定在 startActivity 、或者 startService 來路由跳轉或者啟動服務。拿 startActivity 來說,傳統的路由有什麼缺點: startActivity 有兩種用法,一種是顯示的,一種是隱式的,顯示呼叫如下:
<!--1 匯入依賴--> import com.snail.activityforresultexample.test.SecondActivity; public class MainActivity extends AppCompatActivity { void jumpSecondActivityUseClassName(){ <!--顯示的引用Activity類--> Intent intent =new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); }
顯示呼叫的缺點很明顯,那就是必須要強依賴目標Activity的類實現,有些場景,尤其是大型APP元件化開發時候,有些業務邏輯出於安全考慮,並不想被原始碼或aar依賴,這時顯式依賴的方式就無法走通。再來看看隱式呼叫方法。
第一步:manifest中配置activity的 intent-filter ,至少要配置一個action。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.snail.activityforresultexample"> <application ... <activity android:name=".test.SecondActivity"> <intent-filter> <!--隱式呼叫必須配置android.intent.category.DEFAULT--> <category android:name="android.intent.category.DEFAULT"/> <!--至少配置一個action才能通過隱式呼叫--> <action android:name="com.snail.activityforresultexample.SecondActivity" /> <!--可選--> <!-- <data android:mimeType="video/mpeg" android:scheme="http" ... />--> </intent-filter> </activity> </application> </manifest>
第二步:呼叫。
void jumpSecondActivityUseFilter() { Intent intent = new Intent(); intent.setAction("com.snail.activityforresultexample.SecondActivity"); startActivity(intent); }
如果牽扯到資料傳遞寫法上會更復雜一些,隱式呼叫的缺點有如下幾點:
首先manifest中定義複雜,相對應的會導致暴露的協議變的複雜,不易維護擴充套件。
其次,不同Activity都要不同的action配置,每次增減修改Activity都會很麻煩,對比開發者非常不友好,增加了協作難度。
最後,Activity的export屬性並不建議都設定成True,這是降低風險的一種方式,一般都是收歸到一個Activity, DeeplinkActivitiy 統一處理跳轉,這種場景下, DeeplinkActivitiy 就兼具路由功能,隱式呼叫的場景下,新Activitiy的增減勢必每次都要調整路由表,這會導致開發效率降低,風險增加。
可以看到系統原生的路由框架,並沒太多考慮團隊協同的開發模式,多限定在一個模組內部多個業務間直接相互引用,基本都要程式碼級依賴,對於程式碼及業務隔離很不友好。如不考慮之前Dex方法樹超限制,可以認為三方路由框架完全是為了團隊協同而建立的。
4.APP三方路由框架需具備的能力
目前市面上大部分的路由框架都能搞定上述問題,簡單整理下現在三方路由的能力,可歸納如下:
路由表生成能力 :業務元件[UI業務及服務]自動掃描及註冊邏輯,需要擴充套件性好,無需入侵原有程式碼邏輯。
scheme與業務對映邏輯 :無需依賴具體實現,做到程式碼隔離。
基礎路由跳轉能力 :頁面跳轉能力的支援。
服務類元件的支援 :如去某個服務元件獲取一些配置等。
[擴充套件]路由攔截邏輯: 比如登陸,統一鑑權。
可定製的降級邏輯 :找不到元件時的兜底。
可以看下一個典型的Arouter用法,第一步:對新增頁面新增Router Scheme 宣告。
@Route(path = "/test/activity2") public class Test2Activity extends AppCompatActivity { ... }
build階段會根據註解蒐集路由scheme,生成路由表。第二步使用:
ARouter.getInstance() .build("/test/activity2") .navigation(this);
如上,在ARouter框架下,僅需要字串scheme,無需依賴任何Test2Activity就可實現路由跳轉。
5.APP路由框架的實現
路由框架實現的核心是建立scheme和元件[Activity或者其他服務]的對映關係,也就是路由表,並能根據路由表路由到對應元件的能力。其實分兩部分,第一部分路由表的生成,第二部分,路由表的查詢。
路由表的自動生成
生成路由表的方式有很多,最簡單的就是維護一個公共檔案或者類,裡面對映好每個實現元件跟scheme。
不過,這種做法缺點很明顯:每次增刪修改都要都要修改這個表,對於協同非常不友好,不符合解決協同問題的初衷。不過,最終的路由表倒是都是這條路,就是將所有的Scheme蒐集到一個物件中,只是實現方式的差別,目前幾乎所有的三方路由框架都是藉助註解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面程式設計)來實現的,基本流程如下:
其中牽扯的技術有註解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面程式設計)。APT常用的有JavaPoet,主要是遍歷所有類,找到被註解的Java類,然後聚合生成路由表,由於元件可能有很多,路由表可能也有也有多個,之後,這些生成的輔助類會跟原始碼一併被編譯成class檔案,之後利用AOP技術【如ASM或者JavaAssist】,掃描這些生成的class,聚合路由表,並填充到之前的佔位方法中,完成自動註冊的邏輯。
JavaPoet如何蒐集並生成路由表集合?
以ARouter框架為例,先定義Router框架需要的註解如:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) public @interface Route { /** * Path of route */ String path();
該註解用於標註需要路由的元件,用法如下:
@Route(path = "/test/activity1", name = "測試用 Activity") public class Test1Activity extends BaseActivity { @Autowired int age = 10;
之後利用APT掃描所有被註解的類,生成路由表,實現參考如下:
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (CollectionUtils.isNotEmpty(annotations)) { <!--獲取所有被Route.class註解標註的集合--> Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class); <!--解析並生成表--> this.parseRoutes(routeElements); ... return false; } <!--生成中間路由表Java類--> 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);
產物如下:包含路由表,及區域性註冊入口。
自動註冊:ASM蒐集上述路由表並聚合插入Init程式碼區。
為了能夠插入到 Init 程式碼區,首先需要預留一個位置,一般定義一個空函式,以待後續填充:
public class RouterInitializer { public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) { ... loadRouterTables(); } //自動註冊程式碼 public static void loadRouterTables() { } }
首先利用AOP工具,遍歷上述APT中間產物,聚合路由表,並註冊到預留初始化位置,遍歷的過程牽扯是gradle transform的過程。
蒐集目標,聚合路由表
/**掃描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() }
對目標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() } //結束 jarOutputStream.close() jarFile.close() if (originFile?.exists() == true) { Files.delete(originFile.toPath()) } optJar.renameTo(originFile) }
最終 RouterInitializer.class 的 loadRouterTables 會被修改成如下填充好的程式碼:
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"); ... }
如此就完成了路由表的蒐集與註冊,大概的流程就是如此。當然對於支援服務、Fragment等略有不同,但大體類似。
Router框架對服務類元件的支援
通過路由的方式獲取服務屬於APP路由比較獨特的能力,比如有個使用者中心的元件,我們可以通過路由的方式去查詢使用者是否處於登陸狀態,這種就不是狹義上的頁面路由的概念,通過一串字串如何查到對應的元件並呼叫其方法呢?這種的實現方式也有多種,每種實現方式都有自己的優劣。
一種是可以將服務抽象成介面,沉到底層,上層實現通過路由方式對映物件。
一種是將實現方法直接通過路由方式對映。
先看第一種,這種事Arouter的實現方式,它的優點是所有對外暴露的服務都暴露介面類【沉到底層】,這對於外部的呼叫方,也就是服務使用方非常友好,示例如下:
先定義抽象服務,並沉到底層
public interface HelloService extends IProvider { void sayHello(String name); }
實現服務,並通過Router註解標記。
@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獲取服務例項,並對映成抽象類,然後直接呼叫方法。
((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");
這種實現方式對於使用方其實是很方便的,尤其是一個服務有多個可操作方法的時候,但是缺點是擴充套件性,如果想要擴充套件方法,就要改動底層庫。
再看第二種:將實現方法直接通過路由方式對映
服務的呼叫都要落到方法上,參考頁面路由,也可以支援方法路由,兩者並列關係,所以主要增加一個方法路由表,實現原理與Page路由類似,跟上面的Arouter對比,不用定義抽象層,直接定義實現即可:
定義Method的Router
public class HelloService { <!--引數 name--> @MethodRouter(url = {"arouter://sayhello"}) public void sayHello(String name) { Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show(); }
使用即可
RouterCall.callMethod("arouter://sayhello?name=hello");
上述的缺點就是對於外部呼叫有些複雜,尤其是處理引數的時候,需要嚴格按照協議來處理,優點是,沒有抽象層,如果需要擴充套件服務方法,不需要改動底層。
上述兩種方式各有優劣,不過,如果從做服務元件的初衷出發,第一種比較好:對於呼叫方比較友好。另外對於CallBack的支援,Arouter的處理方式可能也會更方便一些,可以比較方便的交給服務方定義。如果是第二種,服務直接通過路由對映的方式,處理起來就比較麻煩,尤其是Callback中的引數,可能要統一封裝成JSON並維護解析的協議,這樣處理起來,可能不是很好。
路由表的匹配
路由表的匹配比較簡單,就是在全域性Map中根據String輸入,匹配到目標元件,然後依賴反射等常用操作,定位到目標。
6.元件化與路由的關係
元件化是一種開發整合模式,更像一種開發規範,更多是為團隊協同開發帶來方便。元件化最終落地是一個個獨立的業務及功能元件,這些元件之間可能是不同的團隊,處於不同的目的在各自維護,甚至是需要程式碼隔離,如果牽扯到元件間的呼叫與通訊,就不可避免的藉助路由,因為實現隔離的,只能採用通用字串scheme進行通訊,這就是路由的功能範疇。
元件化需要路由支撐的根本原因: 元件間程式碼實現的隔離 。
總結
路由不是一個APP的必備功能,但是大型跨團隊的APP基本都需要。
路由框架的基本能力:路由自動註冊、路由表蒐集、服務及UI介面路由及攔截等核心功能。
元件化與路由的關係:元件化的程式碼隔離導致路由框架成為必須。
為了防止失聯,歡迎關注我的小號
微信改了推送機制,真愛請星標本公號 :point_down:
- 厲害了!自己寫個App 啟動任務框架
- 一個解決滑動衝突的新思路,做到檢視之間無縫地巢狀滑動!
- 谷歌官方改了兩次的知識點,你一定要知道!
- Android 最新架構詳解 | MVI = 響應式程式設計 單向資料流 唯一可信資料來源 !
- 說兩件事~
- 最新的動畫布局來了,一文帶你瞭解!
- Gradle:你必須掌握的開發常見技巧~
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼!
- 厲害了,Android自定義樹狀圖控制元件來了!
- 一文帶你全面掌握Android元件化核心!
- 為什麼大廠開始全面轉向Compose?
- 谷歌限制俄羅斯使用Android系統,俄或將轉用 HarmonyOS!
- 鴻蒙OS、安卓、iOS測試對比,結果出乎意料!
- 最詳細的Android圖片壓縮攻略,讓你一次過足癮(建議收藏)
- Android字型漸變效果實戰!
- 攔截控制元件點選 - 巧用ASM處理防抖!
- Android正確的保活方案,拒絕陷入需求死迴圈!
- 再見 MMKV,自己擼一個FastKV,快的一批
- 白嫖一個Android專案的類圖生成工具!(建議收藏)
- 日常需求做的挺好,面試就被底層原理放倒