前言
本文程式碼在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框架不會這麼簡單的啦。