
前言
目前業(yè)界已經(jīng)有很多成熟的路由框架,最著名的應(yīng)該是 ARouter,那么我們今天為什么還要重新造輪子呢?
我個人覺得有以下原因:
- ARouter 過于強(qiáng)大,很多功能我們不一定用得上,而且不一定適合我們的項目,自己擼一個,可以在滿足項目需求的情況下,功能上去繁就簡。
- 實(shí)踐出真知,我想這也是很多開發(fā)者重復(fù)造輪子的主要原因吧。我們經(jīng)常閱讀許多大牛對于優(yōu)秀框架的剖析,但那也只是大牛的理解,我們自己的呢?
- 便于排查問題。使用開源框架遇到問題一般會耗費(fèi)更多的排查時間,因為我們對源碼“不夠熟悉”,而自己擼的一般都可以快速定位問題。
準(zhǔn)備
進(jìn)入正題前,我們先預(yù)告一下接下來會涉及到的知識點(diǎn)
- Kotlin,本文代碼主要基于 Kotlin 語言編寫,相信大家都知道 Kotlin 的好處了吧?
- APT,即 Annotation Processing Tool,注解處理器,用于在編譯時掃描和處理注解,即解析和保存路由信息。
- 攔截器機(jī)制,眾所周知 OKHTTP 的攔截器機(jī)制是十分強(qiáng)大的,我們也將參考并沿用這套機(jī)制。
正文
使用注解處理器,一般需要3個 Module:
- annotation - 包含注解類,提供給 compiler、api 和 app 使用
- compiler - 編譯器,即注解處理器,在打包時處理注解
- api - 提供路由的 api 接口
注解 Module
新建 Java Module
創(chuàng)建 Router 注解
/**
* 標(biāo)記路由信息,僅支持 Activity
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Router(
/**
* URL path,可以為 "" 或者以 "/" 開頭,例如 "/example\\.html",支持正則表達(dá)式,注意轉(zhuǎn)義
*/
val value: String,
/**
* URL scheme,不包含 "://",例如 "http",支持正則表達(dá)式,注意轉(zhuǎn)義
*/
val scheme: String = "(http|https|native|domain)",
/**
* URL host,不包含 "/",例如 "www\\.google\\.com",支持正則表達(dá)式,注意轉(zhuǎn)義
*/
val host: String = "(\\w+\\.)*domain\\.com",
/**
* 是否需要登錄,默認(rèn)不需要
*
* 需要調(diào)用 [CRouter#setLoginProvider] 才能生效
*/
val needLogin: Boolean = false
)
提供以下參數(shù)
- value: 路由路徑,即 path,為了方便這里直接用 value,不用顯式指定參數(shù)名
- scheme、host: 這兩個即是字面意思,提供默認(rèn)值,一般使用默認(rèn)值即可
- needLogin: 用于登錄攔截,攔截機(jī)制下篇會講到
注意一點(diǎn),這里為了便于匹配,這里 scheme、host、path 都支持正則表達(dá)式,這樣一條規(guī)則可以匹配 N 多鏈接,也可以支持參數(shù)在 path 中的鏈接形式,不過要注意對于特殊字符的轉(zhuǎn)義
舉個栗子,要支持如下鏈接
https://www.wanandroid.com/blog/show/2657
參數(shù)文章 ID 是 2657,那么 path 就可以寫為
/bolg/show/\\d+
看一下在 Activity 中的使用
@Router("/home/rankList")
class RankListActivity : BaseActivity() {
......
}
注解處理 Module
新建 Java Module,和上一步類似,這里不再截圖
在 Module build.gradle 中添加以下依賴
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'com.google.auto.service:auto-service:1.0-rc6'
implementation 'com.squareup:javapoet:1.11.1'
implementation project(':crouter-annotation')
}
- auto-service: Google 出品,用于自動注冊注解處理器
- javapoet: square 大廠的杰作,用于便捷的生成 Java 文件
接下來新建 RouterProcessor
@AutoService(Processor::class)
class RouterProcessor : AbstractProcessor() {
override fun getSupportedAnnotationTypes(): MutableSet<String> {
val supportAnnotationTypes = mutableSetOf<String>()
supportAnnotationTypes.add(Router::class.java.canonicalName)
return supportAnnotationTypes
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
return false
}
}
繼承自 AbstractProcessor,表明是一個注解處理器
添加 AutoService 注解,用于自動生成 META-INF 配置信息
這里遇到一個坑,我使用的是 Android Studio 3.1.4 和 Kotlin 1.2.60,無論如何也不會自動生成 META-INF,導(dǎo)致編譯時無法識別 Processor,最后只能手動添加:
在 src/main 目錄下新建 /resources/META-INF/services/javax.annotation.processing.Processor 目錄和文件
文件內(nèi)容是 Processor 的包名 + 類名
me.wcy.crouter.compiler.RouterProcessor
重寫
getSupportedAnnotationTypes,指定支持的注解類型,即Router::class重寫
getSupportedSourceVersion,指定支持源碼版本,這個是固定模板主要在
process中對注解進(jìn)行處理
確認(rèn)注解生效
為了確認(rèn)我們的注解已經(jīng)創(chuàng)建成功了,我們在 app 中引入注解處理器
app build.gradle
apply plugin: 'kotlin-kapt'
dependencies {
implementation project(':crouter-annotation')
kapt project(':crouter-compiler')
}
Kotlin 中使用 kapt 添加注解處理器
我們在 Processor 的 process 方法中輸出一條日志
private lateinit var messager: Messager
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
// 保存 messager 對象
this.messager = processingEnv.messager
}
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
this.messager.printMessage(Diagnostic.Kind.WARNING, "=============> RouterProcessor 已經(jīng)生效")
return false
}
這里也遇到了一個坑,Kotlin 中 NOTE 及以下級別的日志不會在控制臺打印,所以至少要使用 WARNING 級別以上的日志
不得不說 Kotlin 的坑還是不少的
不過據(jù)說在新版本都已經(jīng)修復(fù)了,我還沒有驗證,大家可以試一下
嘗試一下,Build -> Rebuild Project,然后觀察 build 日志
正常情況下,我們已經(jīng)可以看到 Processor 的日志了,激動
[站外圖片上傳中...(image-5d4e66-1566649332132)]
如果沒有看到日志,需要回過頭一步步排查下哪里沒寫對
收集路由注解
我們已經(jīng)驗證 Processor 有效,下面開始解析路由注解
首先,在 init 中保存需要的對象
private lateinit var filer: Filer
private lateinit var elementUtil: Elements
private lateinit var typeUtil: Types
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
filer = processingEnv.filer
elementUtil = processingEnv.elementUtils
typeUtil = processingEnv.typeUtils
Log.setLogger(processingEnv.messager)
}
這里對日志進(jìn)行封裝,方便使用
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
val routerElements = roundEnv.getElementsAnnotatedWith(Router::class.java)
val activityType = elementUtil.getTypeElement("android.app.Activity")
for (element in routerElements) {
val typeMirror = element.asType()
val router = element.getAnnotation(Router::class.java)
if (typeUtil.isSubtype(typeMirror, activityType.asType())) {
Log.w("[CRouter] Found activity router: $typeMirror")
var routerUrl = ProcessorUtils.assembleRouterUrl(router)
routerUrl = ProcessorUtils.escapeUrl(routerUrl)
}
}
......
}
通過 roundEnv.getElementsAnnotatedWith(Router::class.java) 獲取注解 Router 注解的 Class 信息
遍歷 Class 信息,通過 element.getAnnotation(Router::class.java) 獲取 Router 注解信息,即路由信息,根據(jù)路由信息拼裝路由 URL
路由僅支持 Activity,因此需要排除掉不是 Activity 的 Class
保存路由信息
路由信息已經(jīng)收集完成,接下來要保存到 Java 文件中,那么問題來了,我們首先要先預(yù)想一下保存的 Java 文件的結(jié)構(gòu)是什么樣的?
首先我們要有一個實(shí)體保存路由信息,這里我們可以使用接口
/**
* 真正的路由信息
*/
interface Route {
fun url(): String
fun target(): Class<*>
fun needLogin(): Boolean
}
路由信息最終需要匯總到一個列表中,提供一個接口,用于加載路由信息
/**
* 路由加載器
*/
public interface RouterLoader {
void loadRouter(Set<Route> routeSet);
}
routeSet 由外部傳入,用于保存路由信息
生成的 Java 文件可以實(shí)現(xiàn)該接口,將掃描到的路由信息保存起來
這時有請 javapoet 登場
/**
* Method: @Override public void loadRouter(Set<Route> routerSet)
*/
val loadRouterMethodBuilder = MethodSpec.methodBuilder(ProcessorUtils.METHOD_NAME)
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
.addParameter(groupParamSpec)
for (element in routerElements) {
val typeMirror = element.asType()
val router = element.getAnnotation(Router::class.java)
if (typeUtil.isSubtype(typeMirror, activityType.asType())) {
Log.w("[CRouter] Found activity router: $typeMirror")
val activityCn = ClassName.get(element as TypeElement)
var routerUrl = ProcessorUtils.assembleRouterUrl(router)
routerUrl = ProcessorUtils.escapeUrl(routerUrl)
/**
* Statement: routerSet.add(RouterBuilder.buildRouter(url, needLogin, target));
*/
loadRouterMethodBuilder.addStatement("\$N.add(\$T.buildRouter(\$N, \$N, \$T.class))", ProcessorUtils.PARAM_NAME,
routerBuilderCn, routerUrl, router.needLogin.toString(), activityCn)
}
}
/**
* Write to file
*/
JavaFile.builder("me.wcy.router.annotation.loader",
TypeSpec.classBuilder(ProcessorUtils.getFileName())
.addJavadoc(ProcessorUtils.JAVADOC)
.addSuperinterface(ClassName.get(RouterLoader::class.java))
.addModifiers(Modifier.PUBLIC)
.addMethod(loadRouterMethodBuilder.build())
.build())
.build()
.writeTo(filer)
這里貼出了主要代碼,主要是創(chuàng)建了一個 Java 類,實(shí)現(xiàn)上面的 RouterLoader 接口,添加 loadRouter 方法,保存路由信息,最后添加注釋、修飾符等屬性,寫入文件,javapoet 的使用不屬于本文范疇,因此不再展開講解,完整代碼可參考源碼
為了方便生成代碼,將構(gòu)造路由信息封裝為一個方法
public class RouterBuilder {
public static Route buildRouter(String url, boolean needLogin, Class target) {
return new Route() {
@NotNull
@Override
public String url() {
return url;
}
@NotNull
@Override
public Class target() {
return target;
}
@Override
public boolean needLogin() {
return needLogin;
}
};
}
}
不知道泥萌有沒有發(fā)現(xiàn),這里出現(xiàn)了 Java 代碼的身影(不對,好像前面就出現(xiàn)了,算了,我也懶得找了??),不是說好用 Kotlin 嗎,欺騙感情?
少俠請息怒,真的不是我欺騙大家感情,我也想全程 Kotlin 啊,可是
javapoet他不支持 Kotlin 啊...
生成的 Java 文件使用固定包名 me.wcy.router.annotation.loader,生成類名的方法
fun getFileName(): String {
return "RouterLoader" + "_" + UUID.randomUUID().toString().replace("-", "")
}
大家不妨思考一下,這里為什么使用 RouterLoader + UUID 的方式生成類名?
是因為對于多 Module 項目,每個 Module 都需要收集路由信息,使用隨機(jī)命名防止被覆蓋
這時有些同學(xué)站起來了:隨機(jī)類名看著太亂,如果我想以 Module 的名字命名怎么辦?
好問題!
如果想要根據(jù) Module 命名,可以利用 kapt 設(shè)置 Module 的參數(shù),在 Processor 的 init 方法中讀取參數(shù) 官方文檔傳送門
- 在使用 apt 的 Module 的 build.gradle 中添加
android {
}
kapt {
arguments {
arg("moduleName", project.name)
}
}
到這里,我們完成了路由信息解析和創(chuàng)建 Java 文件保存路由信息,下面讓我們 Rebuild 一下
正常情況下,我們已經(jīng)可以在 app/build/generated/source/kapt/debug/me/wcy/router/annotation/loader 下看到我們在編譯器生成的 Java 文件了
打開看一下內(nèi)容
/**
* DO NOT EDIT THIS FILE! IT WAS GENERATED BY CROUTER.
*/
public class RouterLoader_52def16bb9fa438ca17fec7b3b3f6787 implements RouterLoader {
@Override
public void loadRouter(Set<Route> routerSet) {
routerSet.add(RouterBuilder.buildRouter("(http|https|native|domain)://(\\w+\\.)*domain\\.com", false, HomeActivity.class));
routerSet.add(RouterBuilder.buildRouter("(http|https|native|domain)://(\\w+\\.)*domain\\.com/home/rankList", false, RankListActivity.class));
routerSet.add(RouterBuilder.buildRouter("(http|https|native|domain)://(\\w+\\.)*domain\\.com/home/newTask", false, NewerTaskActivity.class));
}
}
大功告成!
文章篇幅所限,本文暫且講到這里,敬請期待下篇 「手?jǐn)]一個 Router 框架(上):路由攔截機(jī)制」
總結(jié)
本文是 手?jǐn)]一個 Router 框架 的上篇,主要講了 APT 在 Kotlin 環(huán)境下的使用,并實(shí)現(xiàn)了一個完整的 APT 框架。小弟資歷有限,如果那哪里說得不對,還望各位大哥指出??
如果覺得本文對你有幫助,還請不吝賜贊??