手把手教你寫Router框架入門篇

渣渣008發表於2017-12-13

前言

本文程式碼在Github上面,可以自行檢視,程式碼還是挺簡單的。

最近專案在元件化,模組內的跳轉使用了Router框架,通訊暫時還有用的介面,沒想到好的方法。關於元件化和Router框架是什麼這裡不作介紹請自行Google。 Router框架的好處可以解耦,還有在Push的時候可能會比較方便,在後臺直接配置Activity的名稱硬編碼並不好,還有一個是一般現在的客戶端,App裡面的模組都有Web版的,使用Router框架的話,在跳轉的過程中,如果本地模組沒有可以去自動做些處理,使用瀏覽器開啟對應的模組的。 這篇文章著重說一下編譯時候的註解處理器,其實註解的使用在Router框架中的佔比非常小,主要的處理還是在Router庫裡面。由於我自己並不熟悉編譯時的註解的寫法,所以開篇的文章先寫一下註解處理器的使用。

不使用註解處理器的基本Router框架的寫法

路由框架,顧名思義就是通過一個路由地址可以跳轉到對應的頁面去。這裡的頁面只針對Activity,Fragment其實沒必要,個人感覺Fragment最好通過Activity的引數處理。 為了對應起Activity和路由地址,我們需要有一個表來進行記錄,這裡路由地址最好制定好相關的協議,我是希望一個路由地址大概如下:

scheme://module/介面/params
複製程式碼

路由裡面支援引數對於Push或者Web跳轉到相應的介面是非常方便的。 一個不使用註解處理器的路由框架,需要自己手動把每個介面和相應的Activity對應起來。這個處理過程可以在Application裡面做的。 大致步驟: 1.定義介面,讓呼叫著可以去註冊。 2.寫RouterManager類,去實現跳轉。 首先定義一個介面如下:

public interface IRoute {
    void initRouter(Map<String, Class<? extends Activity>> routers);
}
複製程式碼

使用Map將路由地址和Activity關聯起來。 然後完成RouterManager大概如下所示。

public class RouterManager {

    private static volatile RouterManager sManager;
    private Map<String, Class<? extends Activity>> mTables;
    private String mSchemeprefix;

    private RouterManager() {
        mTables = new HashMap<>();
    }

    public static RouterManager getManager() {
        if (sManager == null) {
            synchronized (RouterManager.class) {
                if (sManager == null) {
                    sManager = new RouterManager();
                }
            }
        }
        return sManager;
    }

    public void init(IRoute route) {
        if (route != null) {
            route.initRouter(mTables);
        }
    }

    public void setSetSchemeprefix(String setSchemeprefix) {
        this.mSchemeprefix = setSchemeprefix;
    }

    public void openResult(Context context, String path) {
        if (!TextUtils.isEmpty(mSchemeprefix)) {
            // router://activity/main
            path = mSchemeprefix + "://" + path;
        }
        try {
            Class aClass = mTables.get(path);
            Intent intent = new Intent(context, aClass);
            if (!(context instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            context.startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
複製程式碼

使用者只需要在Application裡面呼叫init方法即可。

public class RouterApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        initRouter();
    }

    private void initRouter() {
        RouterManager manager = RouterManager.getManager();
        manager.setSetSchemeprefix("router");
        manager.init(new IRoute() {
            @Override
            public void initRouter(Map<String, Class<? extends Activity>> routers) {
                routers.put("router://activity/main", MainActivity.class);
                routers.put("router://activity/main2", Main2Activity.class);
                routers.put("router://activity/main3", Main3Activity.class);
            }
        });
    }

}
複製程式碼

相應的程式碼在Github上面。

使用註解處理器自動完成initRouter過程

上面的框架已經基本可以使用了,就是Application裡面的註冊比較麻煩,Activity比較少還好,多的話就比較麻煩了。下面就說一下使用註解處理器自動完成這個過程。

一些基本概念

首先這裡討論的不是在執行時通過反射去呼叫的註解(不過在編寫API的過程中,看想要呼叫者怎麼使用,可能會用到一些反射的),而是在編譯時,掃描位元組碼檔案,根據相應的註解生成特定的程式碼。

註解處理器:一個在javac中,用來編譯時掃描和處理的註解的工具。你可以為特定的註解(你自己寫的註解啦)註冊你自己的註解處理器。

AbstractProcessor

每一個處理器都是繼承於AbstractProcessor。我們可以繼承並複寫一些裡面的方法,這些方法java虛擬機器會自動呼叫的。

public class MyProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment env){ }
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
    @Override
    public Set<String> getSupportedAnnotationTypes() { }
    @Override
    public SourceVersion getSupportedSourceVersion() { }
}
複製程式碼

init(ProcessingEnvironment processingEnv)這個方法會被註解處理工具呼叫,並傳入ProcessingEnvironment引數,這個引數攜帶啦很多有用的資訊後面會用到。 process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)處理函式,我們需要在這個函式裡面寫處理特定註解的程式碼,並生成相應的java檔案,RoundEnvironment通過這個引數我們可以拿到帶有特定註解的元素(類,欄位,方法啦)。 getSupportedAnnotationTypes 指定當前的註解處理器處理哪些註解,從返回值可以看到是返回Set的,那就是一個註解處理器可以處理多個註解的。 getSupportedSourceVersion 指定你使用的java版本,通常這裡直接返回SourceVersion.latestSupported(),可以看這個方法的具體實現。

在java 7中,可以使用註解來代替最後的兩個方法。

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.ai.router.anno.Route")
複製程式碼

不過還是建議使用複寫的方式吧。

註冊你的處理器

在你提供的jar包中,需要有特定的檔案在META-INF/services中,檔名是javax.annotation.processing.Processor內容是處理器的路徑,多個的就沒行一個啦。類似下面。

註冊處理器

處理器的編寫

其實編寫編譯時註解處理器,首先要知道我們要幹什麼,上面一開始就把我們要幹什麼事情分析的很清楚了,我們需要寫一個類,這個類去實現IRoute介面,在initRouter方法裡面去實現關聯Activity和URL。

注意

這個類實現完了,我們怎麼呼叫,這個要想明白先。

  • 1.讓使用者手動呼叫,因為這個類在MakeProject的時候是生成了的,並且類名我們都寫死了,所以可以讓使用者手動呼叫,這就會比較麻煩一些,我們只生成一個類,還好,如果生成的多了,使用者可能會崩潰。
  • 2.在提供對外使用的API中使用反射呼叫,原因和上面一樣,類名是死的,就算不是死的,我們也知道類名的生成規則的。這就是上面提到的可能會用到一些反射的原因。

編寫編譯時註解的library是有一定套路的,一般編寫這種庫,會建三個module,一個是隻存放註解的庫,一個是註解處理庫(這個只是處理註解,並不會增加apk的大小啦),一個是提供對外使用的API庫,前面已經說到,其實這種庫,註解處理器的作用很小的,只是提供某一個功能,其餘的99%的功能都是對外使用的API庫做的(就是說可以只有API庫,其餘的需要編譯時註解處理的工作可以手動處理)。這個很重要,要記住。一般專案劃分如下。

專案結構劃分

註解庫實現

註解庫只專注提供註解給API庫和註解處理器庫使用,本身並不做其他的操作。這裡我們只需要一個註解即可,這個註解是使用在Activity上面的,就是類(繼承自Activity的類)上面,所以這裡就很簡單啦。

package com.ai.router.anno;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String value();
}

複製程式碼

是不是覺得很簡單(當然,這篇文章只是個入門篇,其實就算是隻使用一個註解類,應該也不會這麼簡單的使用一個引數啦,這個其實跟你的路由框架的架構設計有關的,你的路由庫準備怎樣設計使用的路由,要不要使用scheme,要不要二級path,這個都是需要提前想好整個大體的框架,然後畫個圖,自己好好研究,寫個庫哪這麼簡單?)。

註解處理庫實現

其實程式碼也特別少,這裡先貼出程式碼,然後慢慢講解的。

package com.ai.router.compiler;

// @AutoService(Processor.class) // 生成META-INF等資訊
// @SupportedSourceVersion(SourceVersion.RELEASE_7)
// @SupportedAnnotationTypes("com.ai.router.anno.Route")
public class RouterProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        UtilManager.getMgr().init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        UtilManager.getMgr().getMessager().printMessage(Diagnostic.Kind.NOTE, "process");

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
        List<TargetInfo> targetInfos = new ArrayList<>();
        for (Element element : elements) {
            // 檢查型別
            if (!Utils.checkTypeValid(element)) continue;
            TypeElement typeElement = (TypeElement) element;
            Route route = typeElement.getAnnotation(Route.class);
            targetInfos.add(new TargetInfo(typeElement, route.value()));
        }
        if (!targetInfos.isEmpty()) {
            generateCode(targetInfos);
        }
        return false;
    }

    /**
     * 生成對應的java檔案
     *
     * @param targetInfos 代表router和activity
     */
    private void generateCode(List<TargetInfo> targetInfos) {
        // Map<String, Class<? extends Activity>> routers

        TypeElement activityType = UtilManager
                .getMgr()
                .getElementUtils()
                .getTypeElement("android.app.Activity");

        ParameterizedTypeName actParam = ParameterizedTypeName.get(ClassName.get(Class.class),
                WildcardTypeName.subtypeOf(ClassName.get(activityType)));

        ParameterizedTypeName parma = ParameterizedTypeName.get(ClassName.get(Map.class),
                ClassName.get(String.class), actParam);

        ParameterSpec parameterSpec = ParameterSpec.builder(parma, "routers").build();

        MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(Constants.ROUTE_METHOD_NAME)
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(parameterSpec);
        for (TargetInfo info : targetInfos) {
            methodSpecBuilder.addStatement("routers.put($S, $T.class)", info.getRoute(), info.getTypeElement());
        }

        TypeElement interfaceType = UtilManager
                .getMgr()
                .getElementUtils()
                .getTypeElement(Constants.ROUTE_INTERFACE_NAME);

        TypeSpec typeSpec = TypeSpec.classBuilder(Constants.ROUTE_CLASS_NAME)
                .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());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

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

    /**
     * java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
複製程式碼
詳細分析
  • 1.init 前面說啦,ProcessingEnvironment會攜帶一些有用的東西,我們後面需要用到,這裡就把這些物件取出來,放在一個單獨的類裡面方便隨時呼叫。 UtilManager的實現如下,很簡單。
public class UtilManager {
    /**
     * 一個用來處理TypeMirror的工具類
     */
    private Types typeUtils;
    /**
     * 一個用來處理Element的工具類
     */
    private Elements elementUtils;
    /**
     * 正如這個名字所示,使用Filer你可以建立檔案
     */
    private Filer filer;
    /**
     * 日誌相關的輔助類
     */
    private Messager messager;

    private static UtilManager mgr = new UtilManager();

    public void init(ProcessingEnvironment environment) {
        setTypeUtils(environment.getTypeUtils());
        setElementUtils(environment.getElementUtils());
        setFiler(environment.getFiler());
        setMessager(environment.getMessager());
    }

    private UtilManager() {
    }
}
複製程式碼

上面的註釋也很清晰了,具體的怎麼用,要到用到的時候才能理解。比如我們可以通過Elements.getTypeElement("android.app.Activity")得到Activity的type element,這個非常有作用,後面就會看到。 還可以通過Messager.printMessage()方法輸出一些我們想要的資訊。 在註解處理的過程,原始碼的每個部分都是特定的Element。 如下:

package com.example;    // PackageElement
public class Foo {        // TypeElement
    private int a;      // VariableElement
    private Foo other;  // VariableElement
    public Foo () {}    // ExecuteableElement
    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}
複製程式碼
  • 2.process 首先,我們要得到被Route註解的Activity的Element集合,顯然我們規定了Route是作用於類上面的,即上面的TypeElement,還有不僅是作用於類,而且是Activity的子類上面。 Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);這句程式碼即是獲取所有被Route註解的Element的。 if (!Utils.checkTypeValid(element)) continue; 這個是檢測被Route修飾的Element是不是類(TypeElement),並且是不是Activity的字類啦。檢測的方法這裡就不分析啦,GitHub上面的程式碼有註釋,可以去看看。
TypeElement typeElement = (TypeElement) element;
Route route = typeElement.getAnnotation(Route.class);
targetInfos.add(new TargetInfo(typeElement, route.value()));
複製程式碼

這三句程式碼,比較重要,我們來分析一下,一個被Route註釋的類表示一個需要被新增到路由表裡面的資料。這裡使用List記錄起來。typeElement就是那個Activity的字類,route.value()就是Activity的路由地址。

  • 3.generateCode生成java檔案 我們其實已經知道我們最終需要的檔案是什麼樣子的啦。
package com.ai.router.impl;
public class AppRouter implements IRoute {
  @Override
  public void initRouter(Map<String, Class<? extends Activity>> routers) {
    routers.put("router://activity/main2", Main2Activity.class);
    routers.put("router://activity/main3", Main3Activity.class);
    routers.put("router://activity/main", MainActivity.class);
  }
}
複製程式碼

然後寫相應的程式碼即可,這裡生成java檔案,我使用的是square的開源專案javapoet關於它的用法這裡就不說了,這個不重要,可以自行搜尋用法。

  • 4.Router API庫編寫 上面一直說要實現的介面,其實定義在這個庫裡面的。
public interface IRoute {
    void initRouter(Map<String, Class<? extends Activity>> routers);
}
複製程式碼

主要的功能實現類。

package com.ai.router;
public class RouterManager {

    private static volatile RouterManager sManager;
    private Map<String, Class<? extends Activity>> mTables;
    private String mSchemeprefix;

    private RouterManager() {
        mTables = new HashMap<>();
    }

    public static RouterManager getManager() {
        if (sManager == null) {
            synchronized (RouterManager.class) {
                if (sManager == null) {
                    sManager = new RouterManager();
                }
            }
        }
        return sManager;
    }

    public void init() {
        try {
            String className = "com.ai.router.impl.AppRouter";
            Class<?> moduleRouteTable = Class.forName(className);
            Constructor constructor = moduleRouteTable.getConstructor();
            IRoute instance = (IRoute) constructor.newInstance();
            instance.initRouter(mTables);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void setSetSchemeprefix(String setSchemeprefix) {
        this.mSchemeprefix = setSchemeprefix;
    }

    public void openResult(Context context, String path) {
        if (!TextUtils.isEmpty(mSchemeprefix)) {
            // router://activity/main
            path = mSchemeprefix + "://" + path;
        }
        try {
            Class aClass = mTables.get(path);
            Intent intent = new Intent(context, aClass);
            if (!(context instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            context.startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

這裡就提供兩個最簡單的功能 init註冊路由框架,init使用的是反射,拿到AppRouter,它實現了IRoute,呼叫就很簡單了,直接呼叫initRouter方法即可。當然這個方法需要使用者在Application裡面去呼叫。 openResult開啟對應的Activity,程式碼很簡單,一看即懂。

  • 5.使用 Application裡面
public class RouterApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initRouter();
    }
    private void initRouter() {
        RouterManager manager = RouterManager.getManager();
        manager.setSetSchemeprefix("router");
        manager.init();
    }
}
複製程式碼

開啟對應的頁面: RouterManager.getManager().openResult(this, "activity/main");

以上,一個最簡單的使用編譯時註解的Router框架就完成了,這篇文章著重講了編譯時註解的使用。一個Router框架不會這麼簡單的啦。

本文程式碼在這裡啦。

相關文章