面試官,怎樣實現 Router 框架?

Android 開發中,組件化,模塊化是一個老生常談的問題。隨着項目複雜性的增加,模塊化是一個必然的趨勢。除非你能忍受改一下代碼,就須要六七分鐘的漫長時間。java

模塊化,組件化隨之帶來的另一個問題是頁面的跳轉問題,因爲代碼的隔離,代碼之間有時候會沒法互相訪問。因而,路由(Router)框架誕生了。git

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

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

  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);
    }
複製代碼

在要跳轉的地方api

Router.getInstance().build("activity/main").navigation(this);
複製代碼

若是想在多 moule 中使用

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

在主 moudle 中,微信

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

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

在 moudle1 中,app

@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,對外屏蔽了一些細節。但其實,他的原理跟咱們上面的原理都是差很少的。

接下來,我也會寫 gradle plugin 相關的文章,並藉助 gradle 實現 Router 框架。有興趣的話能夠關注個人微信公衆號,徐公碼字,謝謝。

相關文章

java Type 詳解

java 反射機制詳解

註解使用入門(一)

Android 自定義編譯時註解1 - 簡單的例子

Android 編譯時註解 —— 語法詳解

帶你讀懂 ButterKnife 的源碼

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

Android 點九圖機制講解及在聊天氣泡中的應用

面試官,怎樣實現 Router 框架?

掃一掃,歡迎關注個人微信公衆號 stormjun94(徐公碼字), 目前是一名程序員,不只分享 Android開發相關知識,同時還分享技術人成長曆程,包括我的總結,職場經驗,面試經驗等,但願能讓你少走一點彎路。

相關文章
相關標籤/搜索