1. 程式人生 > 實用技巧 >Android高工面試:如果需要實現一個 路由(Router)框架,講講你的思路

Android高工面試:如果需要實現一個 路由(Router)框架,講講你的思路

本文首發公眾號:徐公碼字

Android 開發中,元件化,模組化是一個老生常談的問題。隨著專案複雜性的增長,模組化是一個必然的趨勢。除非你能忍受改一下程式碼,就需要六七分鐘的漫長時間。

模組化,元件化隨之帶來的另外一個問題是頁面的跳轉問題,由於程式碼的隔離,程式碼之間有時候會無法互相訪問。於是,路由(Router)框架誕生了。

目前用得比較多的有阿里的 ARouter,美團的 WMRouter,ActivityRouter 等。

今天,就讓我們一起來看一下怎樣實現一個路由框架。 實現的功能有。

  1. 基於編譯時註解,使用方便
  2. 結果回撥,每次跳轉 Activity 都會回撥跳轉結果
  3. 除了可以使用註解自定義路由,還支援手動分配路由
  4. 支援多模組使用,支援元件化使用

使用說明

基本使用

第一步,在要跳轉的 activity 上面註明 path,

@Route(path = "activity/main")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

在要跳轉的地方

Router.getInstance().build("activity/main").navigation(this);

如果想在多 moule 中使用

第一步,使用 @Modules({"app", "sdk"}) 註明總共有多少個 moudle,並分別在 moudle 中註明當前 moudle 的 名字,使用 @Module("") 註解。注意 @Modules({"app", "sdk"}) 要與 @Module("") 一一對應。

在主 moudle 中,

@Modules({"app", "moudle1"})
@Module("app")
public class RouterApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Router.getInstance().init();
    }
}

在 moudle1 中,

@Route(path = "my/activity/main")
@Module("moudle1")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_2);
    }
}

這樣就可以支援多模組使用了。

自定義注入 router

Router.getInstance().add("activity/three", ThreeActivity.class);

跳轉的時候呼叫

Router.getInstance().build("activity/three").navigation(this);

結果回撥

路由跳轉結果回撥。

Router.getInstance().build("my/activity/main", new RouterCallback() {
    @Override
    public boolean beforeOpen(Context context, Uri uri) { 
    // 在開啟路由之前
        Log.i(TAG, "beforeOpen: uri=" + uri);
        return false;
    }

   // 在開啟路由之後(即開啟路由成功之後會回撥)
    @Override
    public void afterOpen(Context context, Uri uri) {
        Log.i(TAG, "afterOpen: uri=" + uri);

    }

    // 沒有找到改 uri
    @Override
    public void notFind(Context context, Uri uri) {
        Log.i(TAG, "notFind: uri=" + uri);

    }

    // 發生錯誤
    @Override
    public void error(Context context, Uri uri, Throwable e) {
        Log.i(TAG, "error: uri=" + uri + ";e=" + e);
    }
}).navigation(this);

startActivityForResult 跳轉結果回撥

Router.getInstance().build("activity/two").navigation(this, new Callback() {
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data);
    }
});

原理說明

實現一個 Router 框架,涉及到的主要的知識點如下:

  1. 註解的處理
  2. 怎樣解決多個 module 之間的依賴問題,以及如何支援多 module 使用
  3. router 跳轉及 activty startActivityForResult 的處理

我們帶著這三個問題,一起來探索一下。

總共分為四個部分,router-annotion, router-compiler,router-api,stub

router-annotion 主要是定義註解的,用來存放註解檔案

router-compiler 主要是用來處理註解的,自動幫我們生成程式碼

router-api 是對外的 api,用來處理跳轉的。

stub 這個是存放一些空的 java 檔案,提前佔坑。不會打包進 jar。

router-annotion

主要定義了三個註解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String path();
}
@Retention(RetentionPolicy.CLASS)
public @interface Modules {
    String[] value();
}
@Retention(RetentionPolicy.CLASS)
public @interface Module {
    String value();
}

Route 註解主要是用來註明跳轉的 path 的。

Modules 註解,註明總共有多少個 moudle。

Module 註解,註明當前 moudle 的名字。

Modules,Module 註解主要是為了解決支援多 module 使用的。


router-compiler

router-compiler 只有一個類 RouterProcessor,他的原理其實也是比較簡單的,掃描那些類用到註解,並將這些資訊存起來,做相應的處理。這裡是會生成相應的 java 檔案。

主要包括以下兩個步驟

  1. 根據是否有 @Modules @Module 註解,然後生成相應的 RouterInit 檔案
  2. 掃描 @Route 註解,並根據 moudleName 生成相應的 java 檔案

註解基本介紹

在講解 RouterProcessor 之前,我們先來了解一下註解的基本知識。

如果對於自定義註解還不熟悉的話,可以先看我之前寫的這兩篇文章。Android 自定義編譯時註解1 - 簡單的例子Android 編譯時註解 —— 語法詳解

public class RouterProcessor extends AbstractProcessor {
    private static final boolean DEBUG = true;
    private Messager messager;
    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        mFiler = processingEnv.getFiler();
        UtilManager.getMgr().init(processingEnv);
    }

    /**
     * 定義你的註解處理器註冊到哪些註解上
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Route.class.getCanonicalName());
        annotations.add(Module.class.getCanonicalName());
        annotations.add(Modules.class.getCanonicalName());
        return annotations;
    }

    /**
     * java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

}

首先我們先來看一下 getSupportedAnnotationTypes 方法,這個方法返回的是我們支援掃描的註解。

註解的處理

接下來我們再一起來看一下 process 方法

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   // 註解為 null,直接返回
   if (annotations == null || annotations.size() == 0) {
       return false;
   }

   UtilManager.getMgr().getMessager().printMessage(Diagnostic.Kind.NOTE, "process");
   boolean hasModule = false;
   boolean hasModules = false;
   // module
   String moduleName = "RouterMapping";
   Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class);
   if (moduleList != null && moduleList.size() > 0) {
       Module annotation = moduleList.iterator().next().getAnnotation(Module.class);
       moduleName = moduleName + "_" + annotation.value();
       hasModule = true;
   }
   // modules
   String[] moduleNames = null;
   Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);
   if (modulesList != null && modulesList.size() > 0) {
       Element modules = modulesList.iterator().next();
       moduleNames = modules.getAnnotation(Modules.class).value();
       hasModules = true;
   }

   debug("generate modules RouterInit annotations=" + annotations + " roundEnv=" + roundEnv);
   debug("generate modules RouterInit hasModules=" + hasModules + " hasModule=" + hasModule);
   // RouterInit
   if (hasModules) { // 有使用 @Modules 註解,生成 RouterInit 檔案,適用於多個 moudle
       debug("generate modules RouterInit");
       generateModulesRouterInit(moduleNames);
   } else if (!hasModule) { // 沒有使用 @Modules 註解,並且有使用 @Module,生成相應的 RouterInit 檔案,使用與單個 moudle
       debug("generate default RouterInit");
       generateDefaultRouterInit();
   }

   // 掃描 Route 註解
   Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
   List<TargetInfo> targetInfos = new ArrayList<>();
   for (Element element : elements) {
       System.out.println("elements =" + elements);
       // 檢查型別
       if (!Utils.checkTypeValid(element)) continue;
       TypeElement typeElement = (TypeElement) element;
       Route route = typeElement.getAnnotation(Route.class);
       targetInfos.add(new TargetInfo(typeElement, route.path()));
   }

   // 根據 module 名字生成相應的 java 檔案
   if (!targetInfos.isEmpty()) {
       generateCode(targetInfos, moduleName);
   }
   return false;
}

,首先判斷是否有註解需要處理,沒有的話直接返回 annotations == null || annotations.size() == 0

接著我們會判斷是否有 @Modules 註解(這種情況是多個 moudle 使用),有的話會呼叫 generateModulesRouterInit(String[] moduleNames) 方法生成 RouterInit java 檔案,當沒有 @Modules 註解,並且沒有 @Module (這種情況是單個 moudle 使用),會生成預設的 RouterInit 檔案。

private void generateModulesRouterInit(String[] moduleNames) {
   MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
           .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
   for (String module : moduleNames) {
       initMethod.addStatement("RouterMapping_" + module + ".map()");
   }
   TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
           .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
           .addMethod(initMethod.build())
           .build();
   try {
       JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, routerInit)
               .build()
               .writeTo(mFiler);
   } catch (Exception e) {
       e.printStackTrace();
   }
}

假設說我們有"app","moudle1" 兩個 moudle,那麼我們最終生成的程式碼是這樣的。

public final class RouterInit {
  public static final void init() {
    RouterMapping_app.map();
    RouterMapping_moudle1.map();
  }
}

如果我們都沒有使用 @Moudles 和 @Module 註解,那麼生成的 RouterInit 檔案大概是這樣的。

public final class RouterInit {
  public static final void init() {
    RouterMapping.map();
  }
}

這也就是為什麼有 stub module 的原因。因為預設情況下,我們需要藉助 RouterInit 去初始化 map。如果沒有這兩個檔案,ide 編輯器 在 compile 的時候就會報錯。

compileOnly project(path: ':stub')

我們引入的方式是使用 compileOnly,這樣的話再生成 jar 的時候,不會包括這兩個檔案,但是可以在 ide 編輯器中執行。這也是一個小技巧。

Route 註解的處理

我們回過來看 process 方法連對 Route 註解的處理。

// 掃描 Route 自己註解
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
List<TargetInfo> targetInfos = new ArrayList<>();
for (Element element : elements) {
    System.out.println("elements =" + elements);
    // 檢查型別
    if (!Utils.checkTypeValid(element)) continue;
    TypeElement typeElement = (TypeElement) element;
    Route route = typeElement.getAnnotation(Route.class);
    targetInfos.add(new TargetInfo(typeElement, route.path()));
}

// 根據 module 名字生成相應的 java 檔案
if (!targetInfos.isEmpty()) {
    generateCode(targetInfos, moduleName);
}

首先會掃描所有的 Route 註解,並新增到 targetInfos list 當中,接著呼叫 generateCode 方法生成相應的檔案。

private void generateCode(List<TargetInfo> targetInfos, String moduleName) {

        MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("map")
//                .addAnnotation(Override.class)
                .addModifiers(Modifier.STATIC)
                .addModifiers(Modifier.PUBLIC);

//                .addParameter(parameterSpec);
        for (TargetInfo info : targetInfos) {
            methodSpecBuilder.addStatement("com.xj.router.api.Router.getInstance().add($S, $T.class)", info.getRoute(), info.getTypeElement());
        }

        TypeSpec typeSpec = TypeSpec.classBuilder(moduleName)
//                .addSuperinterface(ClassName.get(interfaceType))
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .addJavadoc("Generated by Router. Do not edit it!\n")
                .build();
        try {
            JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, typeSpec)
                    .build()
                    .writeTo(UtilManager.getMgr().getFiler());
            System.out.println("generateCode: =" + Constants.ROUTE_CLASS_PACKAGE + "." + Constants.ROUTE_CLASS_NAME);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("generateCode:e  =" + e);
        }

    }

這個方法主要是使用 javapoet 生成 java 檔案,關於 javaposet 的使用可以見官網文件,生成的 java 檔案是這樣的。

package com.xj.router.impl;

import com.xj.arounterdemo.MainActivity;
import com.xj.arounterdemo.OneActivity;
import com.xj.arounterdemo.TwoActivity;

/**
 * Generated by Router. Do not edit it!
 */
public class RouterMapping_app {
  public static void map() {
    com.xj.router.api.Router.getInstance().add("activity/main", MainActivity.class);
    com.xj.router.api.Router.getInstance().add("activity/one", OneActivity.class);
    com.xj.router.api.Router.getInstance().add("activity/two", TwoActivity.class);
  }
}

可以看到我們定義的註解資訊,最終都會呼叫 Router.getInstance().add() 方法存放起來。


router-api

這個 module 主要是多外暴露的 api,最主要的一個檔案是 Router。

public class Router {

    private static final String TAG = "ARouter";

    private static final Router instance = new Router();

    private Map<String, Class<? extends Activity>> routeMap = new HashMap<>();
    private boolean loaded;

    private Router() {
    }

    public static Router getInstance() {
        return instance;
    }

    public void init() {
        if (loaded) {
            return;
        }
        RouterInit.init();
        loaded = true;
    }
}

當我們想要初始化 Router 的時候,代用 init 方法即可。 init 方法會先判斷是否初始化過,沒有初始化過,會呼叫 RouterInit#init 方法區初始化。

而在 RouterInit#init 中,會呼叫 RouterMap_{@moduleName}#map 方法初始化,改方法又呼叫 Router.getInstance().add() 方法,從而完成初始化

router 跳轉回調

public interface RouterCallback {

    /**
     * 在跳轉 router 之前
     * @param context
     * @param uri
     * @return
     */
    boolean beforeOpen(Context context, Uri uri);

    /**
     * 在跳轉 router 之後
     * @param context
     * @param uri
     */
    void afterOpen(Context context, Uri uri);

    /**
     * 沒有找到改 router
     * @param context
     * @param uri
     */
    void notFind(Context context, Uri uri);

    /**
     * 跳轉 router 錯誤
     * @param context
     * @param uri
     * @param e
     */
    void error(Context context, Uri uri, Throwable e);
}
public void navigation(Activity context, int requestCode, Callback callback) {
    beforeOpen(context);
    boolean isFind = false;
    try {
        Activity activity = (Activity) context;
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(context.getPackageName(), mActivityName));
        intent.putExtras(mBundle);
        getFragment(activity)
                .setCallback(callback)
                .startActivityForResult(intent, requestCode);
        isFind = true;
    } catch (Exception e) {
        errorOpen(context, e);
        tryToCallNotFind(e, context);
    }

    if (isFind) {
        afterOpen(context);
    }

}

private void tryToCallNotFind(Exception e, Context context) {
    if (e instanceof ClassNotFoundException && mRouterCallback != null) {
        mRouterCallback.notFind(context, mUri);
    }
}

主要看 navigation 方法,在跳轉 activity 的時候,首先會會呼叫 beforeOpen 方法回撥 RouterCallback#beforeOpen。接著 catch exception 的時候,如果發生錯誤,會呼叫 errorOpen 方法回撥 RouterCallback#errorOpen 方法。同時呼叫 tryToCallNotFind 方法判斷是否是 ClassNotFoundException,是的話回撥 RouterCallback#notFind。

如果沒有發生 eception,會回撥 RouterCallback#afterOpen。

Activity 的 startActivityForResult 回撥

可以看到我們的 Router 也是支援 startActivityForResult 的

Router.getInstance().build("activity/two").navigation(this, new Callback() {
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ";resultCode=" + resultCode + ";data=" + data);
    }
});

它的實現原理其實很簡單,是藉助一個空白 fragment 實現的,原理的可以看我之前的這一篇文章。

Android Fragment 的妙用 - 優雅地申請許可權和處理 onActivityResult


小結

如果覺得效果不錯的話,請到 github 上面 star, 謝謝。 Router

我們的 Router 框架,流程大概是這樣的。


題外話

看了上面的文章,文章一開頭提到的三個問題,你懂了嗎,歡迎在評論區留言評論。

  1. 註解的處理
  2. 怎樣解決多個 module 之間的依賴問題,以及如何支援多 module 使用
  3. router 跳轉及 activty startActivityForResult 的處理

其實,現在很多 router 框架都藉助 gradle 外掛來實現。這樣有一個好處,就是在多 moudle 使用的時候,我們只需要 apply plugin 就 ok,對外遮蔽了一些細節。但其實,他的原理跟我們上面的原理都是差不多的。

文末

紙上說來終覺淺,時間比較充裕的小夥伴建議到我的B站觀看視訊講解:APT技術實現元件路由框架

歡迎關注我的簡書,分享Android乾貨,交流Android技術。
對文章有何見解,或者有何技術問題,都可以在評論區一起留言討論,我會虔誠為你解答。
也歡迎大家來我的B站找我玩,有各類Android架構師進階技術難點的視訊講解
B站直通車:https://space.bilibili.com/544650554