ARouter原理解析之仿ARouter自定義路由框架

ARouter是什麼?

ARouter是阿里開源的一款android路由框架,幫助 Android App 進行組件化改造的路由框架 —— 支持模塊間的路由、通信、解耦;結合路由可以實現組件化。

ARouter接入指北

完整Arouter接入指南,ARouter重度用戶可以跳過,直接往後看

  • 第一步,根build.gradle設置使用arouter-register
apply plugin: 'com.alibaba.arouter'
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "com.alibaba:arouter-register:?"
    }
}
  • 第二步,創建baselib,並加入dependencies
api 'com.alibaba:arouter-api:x.x.x'
  • 第三步,創建組件module,例如login 或者setting 組件
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    // 替換成最新版本, 需要注意的是api
    // 要與compiler匹配使用,均使用最新版可以保證兼容
    //compile 'com.alibaba:arouter-api:x.x.x' 此移動到baselib中
    api project(path: ':baselib')
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
    ...
}

  • 第四步,通過註解@Route 註冊頁面
// 在支持路由的頁面上添加註解(必選)
// 這裏的路徑需要注意的是至少需要有兩級,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}
  • 第五步,初始化
if (isDebug()) {           // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
    ARouter.openLog();     // 打印日誌
    ARouter.openDebug();   // 開啓調試模式(如果在InstantRun模式下運行,必須開啓調試模式!線上版本需要關閉,否則有安全風險)
}
ARouter.init(mApplication); // 儘可能早,推薦在Application中初始化
  • 第六步,使用ARouter
ARouter.getInstance().build("/test/activity").navigation();

ARouter比傳統Intent有哪些優點

傳統intent的優點

  • 輕量
  • 簡單

傳統intent的缺點

  • 跳轉過程無法控制,一旦調用了startActivity(Intent)便交由系統執行,中間過程無法插手
  • 跳轉失敗無法捕獲、降級,出現問題直接拋出異常
  • 顯示Intent中因爲存在直接的類依賴關係,導致耦合嚴重
startActivity(new Intent(MainActivity.this, LoginActivity.class));//強依賴LoginActivity
  • 隱式Intent中會出現規則集中式的管理,導致協作困難,都需要在Manifest中進行配置,導致擴展性比較差
//隱式 比 顯式更強一點,可以在兩個無關子module 之間跳轉,由於顯式無法引入包,所以無法完成跳轉
Intent intent = new Intent();
intent.setClassName(MainActivity.this,"com.cnn.loginplugin.ui.login.LoginActivity");//設置包路徑
startActivity(intent);

ARouter優點

  • 模塊間通信(後面講原理)
  • 支持url 跳轉 build("/test/activity").navigation()
  • 支持攔截器
// 比較經典的應用就是在跳轉過程中處理登陸事件,這樣就不需要在目標頁重複做登陸檢查
// 攔截器會在跳轉之間執行,多個攔截器會按優先級順序依次執行
@Interceptor(priority = 8, name = "測試用攔截器")
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
    ...
    callback.onContinue(postcard);  // 處理完成,交還控制權
    // callback.onInterrupt(new RuntimeException("我覺得有點異常"));      // 覺得有問題,中斷路由流程

    // 以上兩種至少需要調用其中一種,否則不會繼續路由
    }

    @Override
    public void init(Context context) {
    // 攔截器的初始化,會在sdk初始化的時候調用該方法,僅會調用一次
    }
}
  • 參數注入,@Autowired註解實現,更方便,需要配合ARouter.getInstance().inject(this);一起使用
      @Autowired
    public String name;
    @Autowired
    int age;
    // 通過name來映射URL中的不同參數
    @Autowired(name = "girl") 
    boolean boy;
    // 支持解析自定義對象,URL中使用json傳遞
    @Autowired
    TestObj obj;
// 使用 withObject 傳遞 List 和 Map 的實現了
    // Serializable 接口的實現類(ArrayList/HashMap)
    // 的時候,接收該對象的地方不能標註具體的實現類類型
    // 應僅標註爲 List 或 Map,否則會影響序列化中類型
    // 的判斷, 其他類似情況需要同樣處理        
    @Autowired
    List<TestObj> list;
    @Autowired
    Map<String, List<TestObj>> map;
  • 支持外部url 跳轉
<activity android:name=".SchemeFilterActivity">
            <!-- Scheme -->
            <intent-filter>
                <data
                    android:host="www.nativie.com"
                    android:scheme="arouter"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>
</activity>
  • 簡單demo,github做簡單靜態界面服務器,並部署到https://oslanka.github.io/statichtml.github.io/,手機瀏覽器打開,並點擊href實現html打通原生,按道理來說,所有未攔截的ARouter路徑,均可被web瀏覽器跳轉,html代碼如下:
<html>
<body>
<p><a href="http://www.360.com/">測試跳轉</a> </p>
<p><a href="arouter://www.nativie.com/login/login">跳轉登錄android-ARouter</a></p>
<p><a href="arouter://www.nativie.com/login/login?username=admin&password=123456">跳轉登錄android-ARouter 帶參數</a></p>
<p><a href="arouter://www.nativie.com/setting/setting">跳轉android-ARouter 設置界面</a></p>
<p><a href="arouter://www.nativie.com/web/web">跳轉android-ARouter 設置界面</a></p>
<p><a href="arouter://www.nativie.com/test/test">跳轉android-ARouter 錯誤路徑</a></p>
</body>
</html>

關於攔截器

  • 攔截器(攔截跳轉過程,面向切面編程)
  • 什麼是面向切面編程AOP?AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,通過預編譯方式和運行期間動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率
// 攔截器會在跳轉之前執行,多個攔截器會按優先級順序依次執行
@Interceptor(priority = 8, name = "測試用攔截器")
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
    ...
    callback.onContinue(postcard);  // 處理完成,交還控制權
    // callback.onInterrupt(new RuntimeException("我覺得有點異常"));      // 覺得有問題,中斷路由流程

    // 以上兩種至少需要調用其中一種,否則不會繼續路由
    }

    @Override
    public void init(Context context) {
    // 攔截器的初始化,會在sdk初始化的時候調用該方法,僅會調用一次
    }
}

動態路由

  • 動態註冊路由信息 適用於部分插件化架構的App以及需要動態註冊路由信息的場景,可以通過 ARouter 提供的接口實現動態註冊 路由信息,目標頁面和服務可以不標註 @Route 註解,注意:同一批次僅允許相同 group 的路由信息註冊
ARouter.getInstance().addRouteGroup(new IRouteGroup() {
        @Override
        public void loadInto(Map<String, RouteMeta> atlas) {
            atlas.put("/dynamic/activity",      // path
                RouteMeta.build(
                    RouteType.ACTIVITY,         // 路由信息
                    TestDynamicActivity.class,  // 目標的 Class
                    "/dynamic/activity",        // Path
                    "dynamic",                  // Group, 儘量保持和 path 的第一段相同
                    0,                          // 優先級,暫未使用
                    0                           // Extra,用於給頁面打標
                )
            );
        }
    });

ARouter詳細API


// 構建標準的路由請求,並指定分組
ARouter.getInstance().build("/home/main", "ap").navigation();
// 構建標準的路由請求,通過Uri直接解析
Uri uri;
ARouter.getInstance().build(uri).navigation();

// 構建標準的路由請求,startActivityForResult
// navigation的第一個參數必須是Activity,第二個參數則是RequestCode
ARouter.getInstance().build("/home/main", "ap").navigation(this, 5);

// 指定Flag
ARouter.getInstance()
    .build("/home/main")
    .withFlags();
    .navigation();

// 獲取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
                    
// 對象傳遞
ARouter.getInstance()
    .withObject("key", new TestObj("Jack", "Rose"))
    .navigation();

// 使用綠色通道(跳過所有的攔截器)
ARouter.getInstance().build("/home/main").greenChannel().navigation();

原理探索

  • ARouter.init 時,通過獲取/data/app/包名/base.apk來篩選出ARouter生成的類,如下圖。
  • 對於Activity類型,跳轉ARouter.getInstance().build("/login/login").navigation();,最終執行的是,如下:
**
     * Start activity
     *
     * @see ActivityCompat
     */
    private void startActivity(int requestCode, Context currentContext, Intent intent, Postcard postcard, NavigationCallback callback) {
        if (requestCode >= 0) {  // Need start for result
            if (currentContext instanceof Activity) {//啓動context 爲Activity
                ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
            } else {
              // 啓動context 爲Application 時,不支持requestCode
                logger.warning(Consts.TAG, "Must use [navigation(activity, ...)] to support [startActivityForResult]");
            }
        } else {//啓動context 爲Application
            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
        }

        if ((-1 != postcard.getEnterAnim() && -1 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
        }

        if (null != callback) { // Navigation over.
            callback.onArrival(postcard);
        }
    }
  • 兩個無關的module 如何跳轉的呢?我們發現最終執行startActivity時,所用的context爲Application,思路是這樣的,子module啓動另外無關子module時,將執行權,交還給主進程/主程序去處理
  • 打開生成路由文檔,AROUTER_GENERATE_DOC="enable",會生成arouter-map-of-xx.json和3個java文件
// 更新 build.gradle, 添加參數 AROUTER_GENERATE_DOC = enable
// 生成的文檔路徑 : build/generated/ap_generated_sources/(debug or release)/com/alibaba/android/arouter/docs/arouter-map-of-${moduleName}.json
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
            }
        }
    }
}
//ARouter映射關係如何生成?Generated出三個文件
//ARouter$$Group$$login
//ARouter$$Providers$$loginplugin
//ARouter$$Root$$loginplugin
    atlas.put("/login/login", RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class, "/login/login", "login", new java.util.HashMap<String, Integer>(){{put("password", 8); put("username", 8); }}, -1, -2147483648));

//map 存映射關係
//static Map<String, RouteMeta> routes = new HashMap<>();
  • 以上三個文件是如何生成的呢?APT是Annotation Processing Tool的簡稱,即註解處理工具,apt是在編譯期對代碼中指定的註解進行解析,然後做一些其他處理(如通過javapoet生成新的Java文件)ARouter使用了兩個庫auto-service javapoet,來實現從註解到代碼的注入,其中auto-service爲註解處理器的庫,javapoet爲代碼生成器

通過例子瞭解APT

首先我們瞭解一下元註解,meta-annotation(元註解)

  • @Target
  TYPE, // 類、接口、枚舉類 
  FIELD, // 成員變量(包括:枚舉常量)
  METHOD, // 成員方法
  PARAMETER, // 方法參
  CONSTRUCTOR, // 構造方法
  LOCAL_VARIABLE, // 局部變量
  ANNOTATION_TYPE, // 註解類
  PACKAGE, // 可用於修飾:包
  TYPE_PARAMETER, // 類型參數,JDK 1.8 新增
  TYPE_USE // 使用類型的任何地方,JDK 1.8 新增
  ```

-  @Retention

```java
  SOURCE,    只在本編譯單元的編譯過程中保留,並不寫入Class文件中。
  CLASS,       在編譯的過程中保留並且會寫入Class文件中,但是JVM在加載類的時候不需要將其加載爲運行時可見的(反射可見)的註解==是JVM在加載類時反射不可見。
  RUNTIME   在編譯過程中保留,會寫入Class文件,並且JVM加載類的時候也會將其加載爲反射可見的註解。
  ```

-  @Documented 註解的作用是:描述在使用 javadoc 工具爲類生成幫助文檔時是否要保留其註解信息.

-  @Inherited 註解的作用是:使被它修飾的註解具有繼承性(如果某個類使用了被@Inherited修飾的註解,則其子類將自動具有該註解)

- 通過元註解我們定義自己的註解

- [AutoService 註解處理器](https://github.com/google/auto/tree/master/service)

      註解處理器是一個在javac中的,用來編譯時掃描和處理的註解的工具。你可以爲特定的註解,註冊你自己的註解處理器。到這裏,我假設你已經知道什麼是註解,並且知道怎麼申明的一個註解類型。

一個註解的註解處理器,以Java代碼(或者編譯過的字節碼)作爲輸入,生成文件(通常是.java文件)作爲輸出。

- 虛處理器`AbstractProcessor`
- `init(ProcessingEnvironment env)`: 【核心】
  每一個註解處理器類都必須有一個空的構造函數。然而,這裏有一個特殊的init()方法,它會被註解處理工具調用,並輸入`ProcessingEnviroment`參數。`ProcessingEnviroment`提供很多有用的工具類`Elements`,`Types`和`Filer`
- `process(Set< ? extends TypeElement> annotations, RoundEnvironment env)`:【核心】
  這相當於每個處理器的主函數main()。你在這裏寫你的掃描、評估和處理註解的代碼,以及生成Java文件
- `getSupportedAnnotationTypes()`
  這裏你必須指定,這個註解處理器是註冊給哪個註解的
- `getSupportedSourceVersion()`
  用來指定你使用的Java版本。通常這裏返回`SourceVersion.latestSupported()`

- APT 所用的代碼生成器:**[JavaPoet](https://github.com/square/javapoet)** is a Java API for generating `.java` source files.(JavaPoet 是一個java api ,爲了生成 .java源文件的)

- 官方helloworld

```java
MethodSpec main = MethodSpec.methodBuilder("main")
  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
  .returns(void.class)
  .addParameter(String[].class, "args")
  .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
  .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
  .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
  .addMethod(main)
  .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
  .build();

javaFile.writeTo(System.out);
  • 通過以上可生成以下java 文件
package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}
  • JavaPoet 主要api
- JavaFile 用於構造輸出包含一個頂級類的Java文件 
- TypeSpec 生成類,接口,或者枚舉  
- MethodSpec 生成構造函數或方法 
- FieldSpec 生成成員變量或字段 
- ParameterSpec  用來創建參數  
- AnnotationSpec 用來創建註解
  • JavaPoet 主要佔位符
- $L(for Literals) 執行結構的字符或常見類型,或TypeSpec, $S(for Strings) 字符, $T(for Types) 類, $N(for Names) 方法 等標識符
  $L>$S
//1.Pass an argument value for each placeholder in the format string to `CodeBlock.add()`. In each example, we generate code to say "I ate 3 tacos"
CodeBlock.builder().add("I ate $L $L", 3, "tacos")
 //2.When generating the code above, we pass the hexDigit() method as an argument to the byteToHex() method using $N:
  MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex")
    .addParameter(int.class, "b")
    .returns(String.class)
    .addStatement("char[] result = new char[2]")
    .addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit)
    .addStatement("result[1] = $N(b & 0xf)", hexDigit)
    .addStatement("return new String(result)")
    .build();
//=======================
public String byteToHex(int b) {
  char[] result = new char[2];
  result[0] = hexDigit((b >>> 4) & 0xf);
  result[1] = hexDigit(b & 0xf);
  return new String(result);
}

//$T for Types
//We Java programmers love our types: they make our code easier to understand. And JavaPoet is on board. It has rich built-in support for types, including automatic generation of import statements. Just use $T to reference types:
.addStatement("return new $T()", Date.class)== return new Date();

實戰-自定義簡易版路由-CRouter

  • 新建name-annotation javaLib,定義CRoute註解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface CRoute {
    String path();
}
  • 新建name-compiler javaLib
1.
dependencies {
    implementation project(path: ':TestRouter-annotation')
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    compileOnly 'com.google.auto.service:auto-service-annotations:1.0-rc7'

    implementation 'com.squareup:javapoet:1.8.0'
}
2.@AutoService(Processor.class)
public class TestRouteProcessor extends AbstractProcessor {
  @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
       //dosomething
    }
   @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
      //dosomething
    }
}
 
  • 業務module執行順序如下
 1. annotationProcessor project(':TestRouter-compiler')
implementation project(':TestRouter-annotation')
2.添加註解@CRoute(path = "/csetting/csetting")
3.編譯運行
4.業務module apt 生成的java 文件,如下:
public final class C$csettingC$csettingHelloWorld {
  public static String holder = "/csetting/csetting:com.cnn.settingplugin.SettingsActivity";

  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}
  • 參考ARouter-init 方法,寫出我們CRouter-init
 /**
     * Init, it must be call before used router.
     */
    public static void init(Application application) {
        if (!hasInit) {
            CRouter.application=application;
            hasInit=true;
            try {
                getFileNameByPackageName(application, ROUTE_ROOT_PAKCAGE);
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
  • 利用反射獲取到註解對應映射關係,並參考ARouter存入HashMap


  • 通過隱式啓動Activity模擬跳轉


  • 到此我們模擬出簡易版本的ARouter,完整自定義CRouter

/**
 * Created by caining on 7/29/21 16:09
 * E-Mail Address:[email protected]
 */
public class CRouter {
    private volatile static CRouter instance = null;
    private volatile static boolean hasInit = false;
    private static Application application;
    public static final String ROUTE_ROOT_PAKCAGE = "com.cnn.crouter";
    private static Map<String ,String> mapHolder = new HashMap<>();

    /**
     * Init, it must be call before used router.
     */
    public static void init(Application application) {
        if (!hasInit) {
            CRouter.application=application;
            hasInit=true;
            try {
                getFileNameByPackageName(application, ROUTE_ROOT_PAKCAGE);
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    /**
     * Get instance of router. A
     * All feature U use, will be starts here.
     */
    public static CRouter getInstance() {
        if (!hasInit) {
            throw new InitException("ARouter::Init::Invoke init(context) first!");
        } else {
            if (instance == null) {
                synchronized (CRouter.class) {
                    if (instance == null) {
                        instance = new CRouter();
                    }
                }
            }
            return instance;
        }
    }


    public void navigation(String path) {
         startActivity(path);
    }

    private void startActivity(String path) {
        String classPath
                = mapHolder.get(path);
        if (!TextUtils.isEmpty(classPath)) {
            Intent intent = new Intent();
            intent.setClassName(application, classPath);//設置包路徑
            ActivityCompat.startActivity(application, intent, null);
        }else {
            Toast.makeText(application, "路徑空啦", Toast.LENGTH_SHORT).show();
        }
    }


    /**
     * 通過指定包名,掃描包下面包含的所有的ClassName
     *
     * @param context     U know
     * @param packageName 包名
     * @return 所有class的集合
     */
    private static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
        final Set<String> classNames = new HashSet<>();

        List<String> paths = getSourcePaths(context);
        final CountDownLatch parserCtl = new CountDownLatch(paths.size());

        for (final String path : paths) {
            DefaultPoolExecutor.getInstance().execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexfile = null;

                    try {
                        if (path.endsWith("EXTRACTED_SUFFIX")) {
                            //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                            dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                        } else {
                            dexfile = new DexFile(path);
                        }

                        Enumeration<String> dexEntries = dexfile.entries();
                        while (dexEntries.hasMoreElements()) {
                            String className = dexEntries.nextElement();
                            if (className.startsWith(packageName)) {
                                classNames.add(className);
                                try {
                                    Class clazz = Class.forName(className);
                                    Object obj = clazz.newInstance();
                                    Field field03 = clazz.getDeclaredField("holder"); // 獲取屬性爲id的字段
                                    String value= (String) field03.get(obj);
                                    String[] split = value.split(":");
                                    if (split!=null&&split.length==2) {
                                        mapHolder.put(split[0],split[1]);
                                    }
                                    Log.i("test-->",mapHolder.toString());
                                } catch (ClassNotFoundException e) {
                                    e.printStackTrace();
                                } catch (IllegalAccessException e) {
                                    e.printStackTrace();
                                } catch (InstantiationException e) {
                                    e.printStackTrace();
                                } catch (SecurityException e) {
                                    e.printStackTrace();
                                } catch (NoSuchFieldException e) {
                                    e.printStackTrace();
                                } catch (IllegalArgumentException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    } catch (Throwable ignore) {
                        Log.e("ARouter", "Scan map file in dex files made error.", ignore);
                    } finally {
                        if (null != dexfile) {
                            try {
                                dexfile.close();
                            } catch (Throwable ignore) {
                            }
                        }

                        parserCtl.countDown();
                    }
                }
            });
        }

        parserCtl.await();

        return classNames;
    }
    private static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
        ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
        List<String> sourcePaths = new ArrayList<>();
        sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
        return sourcePaths;
    }
}

總結

問題

  • 除了ARouter,你知道利用apt 實現的框架都有哪些?
  • ARouter有沒有什麼缺點?

引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章