APP路由框架與組件化分析

前端開發(fā)經(jīng)常遇到一個詞:路由,在Android APP開發(fā)中,路由還經(jīng)常和組件化開發(fā)強關(guān)聯(lián)在一起,那么到底什么是路由,一個路由框架到底應該具備什么功能,實現(xiàn)原理是什么樣的?路由是否是APP的強需求呢?與組件化到底什么關(guān)系,本文就簡單分析下如上幾個問題。

路由的概念

路由這個詞本身應該是互聯(lián)網(wǎng)協(xié)議中的一個詞,維基百科對此的解釋如下:

路由(routing)就是通過互聯(lián)的網(wǎng)絡把信息從源地址傳輸?shù)侥康牡刂返幕顒?。路由發(fā)生在OSI網(wǎng)絡參考模型中的第三層即網(wǎng)絡層。

個人理解,在前端開發(fā)中,路由就是通過一串字符串映射到對應業(yè)務的能力。APP的路由框首先能夠搜集各組件的路由scheme,并生成路由表,然后,能夠根據(jù)外部輸入字符串在路由表中匹配到對應的頁面或者服務,進行跳轉(zhuǎn)或者調(diào)用,并提供會獲取返回值等,示意如下

image.png

所以一個基本路由框架要具備如下能力:

    1. APP路由的掃描及注冊邏輯
    1. 路由跳轉(zhuǎn)target頁面能力
    1. 路由調(diào)用target服務能力

APP中,在進行頁面路由的時候,經(jīng)常需要判斷是否登錄等一些額外鑒權(quán)邏輯所以,還需要提供攔截邏輯等,比如:登陸。

三方路由框架是否是APP強需求

答案:不是,系統(tǒng)原生提供路由能力,但功能較少,稍微大規(guī)模的APP都采用三方路由框架。

Android系統(tǒng)本身提供頁面跳轉(zhuǎn)能力:如startActivity,對于工具類APP,或單機類APP,這種方式已經(jīng)完全夠用,完全不需要專門的路由框架,那為什么很多APP還是采用路由框架呢?這跟APP性質(zhì)及路由框架的優(yōu)點都有關(guān)。比如淘寶、京東、美團等這些大型APP,無論是從APP功能還是從其研發(fā)團隊的規(guī)模上來說都很龐大,不同的業(yè)務之間也經(jīng)常是不同的團隊在維護,采用組件化的開發(fā)方式,最終集成到一個APK中。多團隊之間經(jīng)常會涉及業(yè)務間的交互,比如從電影票業(yè)務跳轉(zhuǎn)到美食業(yè)務,但是兩個業(yè)務是兩個獨立的研發(fā)團隊,代碼實現(xiàn)上是完全隔離的,那如何進行通信呢?首先想到的是代碼上引入,但是這樣會打破了低耦合的初衷,可能還會引入各種問題。例如,部分業(yè)務是外包團隊來做,這就牽扯到代碼安全問題,所以還是希望通過一種類似黑盒的方式,調(diào)用目標業(yè)務,這就需要中轉(zhuǎn)路由支持,所以國內(nèi)很多APP都是用了路由框架的。其次我們各種跳轉(zhuǎn)的規(guī)則并不想跟具體的實現(xiàn)類扯上關(guān)系,比如跳轉(zhuǎn)商詳?shù)臅r候,不希望知道是哪個Activity來實現(xiàn),只需要一個字符串映射過去即可,這對于H5、或者后端開發(fā)來處理跳轉(zhuǎn)的時候,就非常標準。

原生路由的限制:功能單一,擴展靈活性差,不易協(xié)同

傳統(tǒng)的路由基本上就限定在startActivity、或者startService來路由跳轉(zhuǎn)或者啟動服務。拿startActivity來說,傳統(tǒng)的路由有什么缺點:startActivity有兩種用法,一種是顯示的,一種是隱式的,顯示調(diào)用如下:

<!--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);
    }

顯示調(diào)用的缺點很明顯,那就是必須要強依賴目標Activity的類實現(xiàn),有些場景,尤其是大型APP組件化開發(fā)時候,有些業(yè)務邏輯出于安全考慮,并不想被源碼或aar依賴,這時顯式依賴的方式就無法走通。再來看看隱式調(diào)用方法。

第一步: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>
            <!--隱式調(diào)用必須配置android.intent.category.DEFAULT-->
                   <category android:name="android.intent.category.DEFAULT"/>
            <!--至少配置一個action才能通過隱式調(diào)用-->
                <action android:name="com.snail.activityforresultexample.SecondActivity" />
                <!--可選-->
  <!--              <data android:mimeType="video/mpeg" android:scheme="http" ... />-->
            </intent-filter>
        </activity>
    </application>
</manifest>

第二步:調(diào)用

void jumpSecondActivityUseFilter() {
    Intent intent = new Intent();
    intent.setAction("com.snail.activityforresultexample.SecondActivity");
    startActivity(intent);
}

如果牽扯到數(shù)據(jù)傳遞寫法上會更復雜一些,隱式調(diào)用的缺點有如下幾點:

  • 首先manifest中定義復雜,相對應的會導致暴露的協(xié)議變的復雜,不易維護擴展。
  • 其次,不同Activity都要不同的action配置,每次增減修改Activity都會很麻煩,對比開發(fā)者非常不友好,增加了協(xié)作難度。
  • 最后,Activity的export屬性并不建議都設置成True,這是降低風險的一種方式,一般都是收歸到一個Activity,DeeplinkActivitiy統(tǒng)一處理跳轉(zhuǎn),這種場景下,DeeplinkActivitiy就兼具路由功能,隱式調(diào)用的場景下,新Activitiy的增減勢必每次都要調(diào)整路由表,這會導致開發(fā)效率降低,風險增加。

可以看到系統(tǒng)原生的路由框架,并沒太多考慮團隊協(xié)同的開發(fā)模式,多限定在一個模塊內(nèi)部多個業(yè)務間直接相互引用,基本都要代碼級依賴,對于代碼及業(yè)務隔離很不友好。如不考慮之前Dex方法樹超限制,可以認為三方路由框架完全是為了團隊協(xié)同而創(chuàng)建的。

APP三方路由框架需具備的能力

目前市面上大部分的路由框架都能搞定上述問題,簡單整理下現(xiàn)在三方路由的能力,可歸納如下:

  • 路由表生成能力:業(yè)務組件[UI業(yè)務及服務]自動掃描及注冊邏輯,需要擴展性好,無需入侵原有代碼邏輯
  • scheme與業(yè)務映射邏輯 :無需依賴具體實現(xiàn),做到代碼隔離
  • 基礎路由跳轉(zhuǎn)能力 :頁面跳轉(zhuǎn)能力的支持
  • 服務類組件的支持 :如去某個服務組件獲取一些配置等
  • [擴展]路由攔截邏輯:比如登陸,統(tǒng)一鑒權(quán)
  • 可定制的降級邏輯:找不到組件時的兜底

可以看下一個典型的Arouter用法,第一步:對新增頁面添加Router Scheme 聲明,

    @Route(path = "/test/activity2")
    public class Test2Activity extends AppCompatActivity {
         ...
    }

build階段會根據(jù)注解搜集路由scheme,生成路由表。第二步使用

        ARouter.getInstance()
                .build("/test/activity2")
                .navigation(this);

如上,在ARouter框架下,僅需要字符串scheme,無需依賴任何Test2Activity就可實現(xiàn)路由跳轉(zhuǎn)。

APP路由框架的實現(xiàn)

路由框架實現(xiàn)的核心是建立scheme和組件[Activity或者其他服務]的映射關(guān)系,也就是路由表,并能根據(jù)路由表路由到對應組件的能力。其實分兩部分,第一部分路由表的生成,第二部分,路由表的查詢

路由表的自動生成

生成路由表的方式有很多,最簡單的就是維護一個公共文件或者類,里面映射好每個實現(xiàn)組件跟scheme,

image.png

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

image.png

其中牽扯的技術(shù)有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面編程)。APT常用的有JavaPoet,主要是遍歷所有類,找到被注解的Java類,然后聚合生成路由表,由于組件可能有很多,路由表可能也有也有多個,之后,這些生成的輔助類會跟源碼一并被編譯成class文件,之后利用AOP技術(shù)【如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掃描所有被注解的類,生成路由表,實現(xiàn)參考如下:

@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);

產(chǎn)物如下:包含路由表,及局部注冊入口。

image.png

自動注冊:ASM搜集上述路由表并聚合插入Init代碼區(qū)

為了能夠插入到Init代碼區(qū),首先需要預留一個位置,一般定義一個空函數(shù),以待后續(xù)填充:

    public class RouterInitializer {
    
        public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
            ...
            loadRouterTables();
        }
        //自動注冊代碼    
        public static void loadRouterTables() {
        
        }
}

首先利用AOP工具,遍歷上述APT中間產(chǎn)物,聚合路由表,并注冊到預留初始化位置,遍歷的過程牽扯是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()
          }
          //結(jié)束
          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路由比較獨特的能力,比如有個用戶中心的組件,我們可以通過路由的方式去查詢用戶是否處于登陸狀態(tài),這種就不是狹義上的頁面路由的概念,通過一串字符串如何查到對應的組件并調(diào)用其方法呢?這種的實現(xiàn)方式也有多種,每種實現(xiàn)方式都有自己的優(yōu)劣。

  • 一種是可以將服務抽象成接口,沉到底層,上層實現(xiàn)通過路由方式映射對象
  • 一種是將實現(xiàn)方法直接通過路由方式映射

先看第一種,這種事Arouter的實現(xiàn)方式,它的優(yōu)點是所有對外暴露的服務都暴露接口類【沉到底層】,這對于外部的調(diào)用方,也就是服務使用方非常友好,示例如下:

先定義抽象服務,并沉到底層

image.png
public interface HelloService extends IProvider {
    void sayHello(String name);
}

實現(xiàn)服務,并通過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獲取服務實例,并映射成抽象類,然后直接調(diào)用方法。

  ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

這種實現(xiàn)方式對于使用方其實是很方便的,尤其是一個服務有多個可操作方法的時候,但是缺點是擴展性,如果想要擴展方法,就要改動底層庫。

再看第二種:將實現(xiàn)方法直接通過路由方式映射

服務的調(diào)用都要落到方法上,參考頁面路由,也可以支持方法路由,兩者并列關(guān)系,所以組要增加一個方法路由表,實現(xiàn)原理與Page路由類似,跟上面的Arouter對比,不用定義抽象層,直接定義實現(xiàn)即可:

定義Method的Router

    public class HelloService {
        
        <!--參數(shù) 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");

上述的缺點就是對于外部調(diào)用有些復雜,尤其是處理參數(shù)的時候,需要嚴格按照協(xié)議來處理,優(yōu)點是,沒有抽象層,如果需要擴展服務方法,不需要改動底層。

上述兩種方式各有優(yōu)劣,不過,如果從做服務組件的初衷出發(fā),第一種比較好:對于調(diào)用方比較友好。另外對于CallBack的支持,Arouter的處理方式可能也會更方便一些,可以比較方便的交給服務方定義。如果是第二種,服務直接通過路由映射的方式,處理起來就比較麻煩,尤其是Callback中的參數(shù),可能要統(tǒng)一封裝成JSON并維護解析的協(xié)議,這樣處理起來,可能不是很好。

路由表的匹配

路由表的匹配比較簡單,就是在全局Map中根據(jù)String輸入,匹配到目標組件,然后依賴反射等常用操作,定位到目標。

組件化與路由的關(guān)系

組件化是一種開發(fā)集成模式,更像一種開發(fā)規(guī)范,更多是為團隊協(xié)同開發(fā)帶來方便。組件化最終落地是一個個獨立的業(yè)務及功能組件,這些組件之間可能是不同的團隊,處于不同的目的在各自維護,甚至是需要代碼隔離,如果牽扯到組件間的調(diào)用與通信,就不可避免的借助路由,因為實現(xiàn)隔離的,只能采用通用字符串scheme進行通信,這就是路由的功能范疇。

組件化需要路由支撐的根本原因:組件間代碼實現(xiàn)的隔離

總結(jié)

  • 路由不是一個APP的必備功能,但是大型跨團隊的APP基本都需要
  • 路由框架的基本能力:路由自動注冊、路由表搜集、服務及UI界面路由及攔截等核心功能
  • 組件化與路由的關(guān)系:組件化的代碼隔離導致路由框架成為必須

作者:看書的小蝸牛
原文鏈接: APP路由框架與組件化簡析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容