简单的说功能大部分和阿里美团的路由都半斤八两吧,以前公司拿来做组件化拆分的,支持编译时注册以及增量编译等等,整体kt重构过一次。
支持参数跳转,以及startActivityForResult操作,并提供成功失败回掉监听等。
同时项目升级了kapt版本,已经支持kapt的增量编译了。
新增了ksp支持,速度可以比kapt更快,理论上优化25%以上的注解解释器速度,同时ksp由于已经支持增编以及编译缓存,所以性能更好更优异。
使用方法
1、给Activity或RouterCallback添加注解
@BindRouter(urls = {"https://www.github.com"})
public class TestActivity extends Activity {
}
@BindRouter(urls = {"https://www.baidu.com"}, interceptors = {TestInterceptor.class})
public class SimpleCallBack implements RouterCallback {
@Override
public void run(RouterContext context) {
Toast.makeText(context.getContext(), "testing", Toast.LENGTH_SHORT).show();
}
}
2、万一有高仿的路由出现,可以这样
@BindRouter(urls = { "https://github.com/leifzhang"}, weight=10)
public class TestActivity extends Activity {
}
3、启动一个路由跳转
Router.sharedRouter().open("https://github.com/leifzhang", this);
4、复杂的多参数传递可以用这个
val request = KRequest("https://www.baidu.com/test", onSuccess = {
Log.i("KRequest", "onSuccess")
}, onFail = {
Log.i("KRequest", "onFail")
}).apply {
activityResultCode = 12345
}.start(this)
定义注解
package com.zpw.routerannotation
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import kotlin.reflect.KClass
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(RetentionPolicy.CLASS)
annotation class BindRouter(val urls: Array<String>)
解析注解
首先能够根据注解进行跳转。我们需要做的就是在编译时收集添加注解的类,然后获取对应的url和类名保存起来。
这里设计的技术就是APT。
package com.zpw.routercomplier
import com.google.auto.service.AutoService
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.TypeSpec
import com.zpw.routerannotation.BindRouter
import com.zpw.routercomplier.utils.Const
import com.zpw.routercomplier.utils.Logger
import com.zpw.routercomplier.utils.TypeUtils
import org.apache.commons.collections4.CollectionUtils
import org.apache.commons.collections4.MapUtils
import org.apache.commons.lang3.StringUtils
import java.io.IOException
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.ElementKind
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
@AutoService(Processor::class)
class TestProcessor : AbstractProcessor() {
companion object {
private const val TAG = "TestProcessor"
}
private var filer: Filer? = null
private var logger: Logger? = null
private var moduleName: String? = null
private var elementUtils: Elements? = null
private var types: Types? = null
override fun init(env: ProcessingEnvironment) {
super.init(env)
val messager = processingEnv.messager
filer = processingEnv.filer
logger = Logger(messager)
types = processingEnv.typeUtils
elementUtils = processingEnv.elementUtils
val typeUtils = TypeUtils(types, elementUtils)
// 获取对应的模块名,这样子就不会生成重复的类
val options = processingEnv.options
if (MapUtils.isNotEmpty(options)) {
moduleName = options[Const.KEY_MODULE_NAME]
if (StringUtils.isNotEmpty(moduleName)) {
moduleName = moduleName?.replace("[^0-9a-zA-Z_]+".toRegex(), "")
} else {
moduleName = Const.DEFAULT_APP_MODULE
}
logger?.info("The user has configuration the module name, it was [$moduleName]")
}
}
override fun getSupportedAnnotationTypes(): Set<String> {
return setOf(BindRouter::class.java.canonicalName)// 过滤的注解
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
if (CollectionUtils.isNotEmpty(annotations)) {
initRouter(moduleName, roundEnv)// 根据模块名称进行对应的中间类的创建
return true
}
return false
}
private fun initRouter(moduleName: String?, roundEnv: RoundEnvironment) {
// 获取所有添加注解的类
val elements = roundEnv.getElementsAnnotatedWith(BindRouter::class.java)
if (elements.isEmpty()) {
return
}
// 动态生成注册url和类名的静态方法
val initMethod = MethodSpec.methodBuilder("register")
.addModifiers(
Modifier.PUBLIC,
Modifier.FINAL,
Modifier.STATIC
)
for (element in elements) {
//检查element类型
//field type
val router: BindRouter = element.getAnnotation(BindRouter::class.java)
var className: ClassName?
className = if (element.kind == ElementKind.CLASS) {
ClassName.get(element as TypeElement)
} else if (element.kind == ElementKind.METHOD) {
ClassName.get(element.enclosingElement as TypeElement)
} else {
throw IllegalArgumentException("unknow type")
}
//class type
val id: Array<String> = router.urls
for (format in id) {
// 填充方法
initMethod.addStatement(
"com.zpw.routerlib.Router.map(\"$format\",$className.class)"
)
}
}
// 动态生成注册url和类名的类
val moduleName = "RouterInit_$moduleName"
val routerMapping = TypeSpec.classBuilder(moduleName)
.addModifiers(
Modifier.PUBLIC,
Modifier.FINAL
)
.addMethod(initMethod.build())
.build()
try {
// 动态生成注册url和类名的java文件
JavaFile.builder("com.zpw.routerlib.register", routerMapping)
.build()
.writeTo(filer)
} catch (ignored: IOException) {
}
}
}
执行编译之后会在com.zpw.routerlib.register文件夹下生成对应的java文件:
package com.zpw.routerlib.register;
public final class RouterInit_app {
public static final void register() {
com.zpw.routerlib.Router.map("https://www.baidu.com/test",com.zpw.router_android.TestActivity.class);
}
}
注意这里我们调用了com.zpw.routerlib.Router类的静态方法map来保存url和对应类名的映射。
private val router by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Router()
}
fun map(url: String, mClass: Class<out Activity>?, options: RouterOptions = RouterOptions()) {
val uri = Uri.parse(url)
uri?.path?.let {
options.openClass = mClass
val hostParams: HostParams?
if (router.hosts.containsKey(uri.host)) {
router.hosts[uri.host]?.apply {
setRoute(it, options)
}
} else {
uri.host?.let { host ->
hostParams = HostParams(host)
hostParams.setRoute(it, options)
router.hosts[hostParams.host] = hostParams
}
}
}
}
首先将传入的url进行解析,然后将类名保存在RouterOptions类中。RouterOptions类的定义如下:
package com.zpw.routerlib.model
import android.app.Activity
class RouterOptions {
var openClass: Class<out Activity>? = null
}
然后创建host与RouterOptions类的映射,HostParams类的定义如下:
package com.zpw.routerlib.model
import java.util.HashMap
class HostParams(val host: String) {
val routes = HashMap<String, RouterOptions>()
fun setRoute(path: String, options: RouterOptions) {
routes[path] = options
}
fun getOptions(path: String): RouterOptions? {
return routes[path]
}
}
最后创建host与HostParams类的映射,保存在Router的hosts变量中,hosts变量的定义如下:
private val hosts: MutableMap<String, HostParams> = HashMap()
我们在后续的调用中只要传入对应的url,然后就可以根据缓存中的数据检索出其对应的类名进行调用。
那么,既然我们动态生成了如下的注册的java文件,要怎么调用呢?答案是transform+asm机制。
package com.zpw.routerlib.register;
public final class RouterInit_app {
public static final void register() {
com.zpw.routerlib.Router.map("https://www.baidu.com/test",com.zpw.router_android.TestActivity.class);
}
}
我们通过注册Transform的子类,在遍历所有的class文件时,筛选我们生成的java文件的包名:
static boolean checkClassName(String className) {
if (className.contains("R\\$") || className.endsWith("R") || className.endsWith("BuildConfig")) {
return false;
}
String packageList = Constant.REGISTER_PACKAGE_CONST; // "com.zpw.routerlib.register"
return className.contains(packageList);
}
然后修改对应的文件,让他能够调用我们生成的java 文件:
String className = Constant.REGISTER_CLASS_CONST.replace('.', '/');// com.zpw.emptyloader.RouterRegistry
File dest = new File(directory, className + SdkConstants.DOT_CLASS);
if (!dest.exists()) {
try {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM6, writer) {};
cv.visit(50, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
TryCatchMethodVisitor mv = new TryCatchMethodVisitor(cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
Constant.REGISTER_FUNCTION_NAME_CONST, "()V", null, null), null, deleteItems);// "register"
mv.visitCode();
for (String clazz : items) {
String input = clazz.replace(".class", "");
input = input.replace(".", "/");
mv.addTryCatchMethodInsn(Opcodes.INVOKESTATIC, input, Constant.REGISTER_CLASS_FUNCTION_CONST, "()V", false);// "register"
}
mv.visitInsn(Opcodes.RETURN);
mv.visitEnd();
cv.visitEnd();
dest.getParentFile().mkdirs();
new FileOutputStream(dest).write(writer.toByteArray());
} catch (Exception e) {
e.printStackTrace();
}
}
class ClassFilterVisitor extends ClassVisitor {
private HashSet<String> classItems
private HashSet<String> deleteItems
ClassFilterVisitor(ClassVisitor classVisitor, HashSet<String> classItems, HashSet<String> deleteItems) {
super(Opcodes.ASM6, classVisitor)
this.classItems = classItems
this.deleteItems = deleteItems
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name == "register" && desc == "()V") {
TryCatchMethodVisitor methodVisitor = new TryCatchMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions),
classItems, deleteItems)
return methodVisitor
}
return super.visitMethod(access, name, desc, signature, exceptions)
}
}
package com.kronos.autoregister.helper;
import com.kronos.autoregister.Constant;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.HashSet;
public class TryCatchMethodVisitor extends MethodVisitor {
private HashSet<String> deleteItems;
private HashSet<String> addItems;
public TryCatchMethodVisitor(MethodVisitor mv, HashSet<String> addItems, HashSet<String> deleteItems) {
super(Opcodes.ASM5, mv);
this.deleteItems = deleteItems;
this.addItems = addItems;
if (this.addItems == null) {
this.addItems = new HashSet<>();
}
if (this.deleteItems == null) {
this.deleteItems = new HashSet<>();
}
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
String className = owner + ".class";
if (!deleteItems.contains(className)) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
@Override
public void visitCode() {
for (String input : addItems) {
input = input.replace(".class", "");
input = input.replace(".", "/");
deleteItems.add(input + ".class");
addTryCatchMethodInsn(Opcodes.INVOKESTATIC, input, Constant.REGISTER_CLASS_FUNCTION_CONST, "()V", false);
}
super.visitCode();
}
public void addTryCatchMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
mv.visitMethodInsn(opcode, owner, name, desc, itf);
mv.visitLabel(l1);
Label l3 = new Label();
mv.visitJumpInsn(Opcodes.GOTO, l3);
mv.visitLabel(l2);
mv.visitVarInsn(Opcodes.ASTORE, 1);
mv.visitLabel(l3);
}
}
最后生成的代码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.zpw.emptyloader;
import com.zpw.routerlib.register.RouterInit_app;
public class RouterRegistry {
public static void register() {
try {
RouterInit_app.register();
} catch (Exception var2) {
}
}
}
我们只需要在项目中创建一个空壳的类作为中转站,在适当的地方调用即可完成路由的注册。
1、 首先我在路由组件内部用compileOnly的方式引入了一个注册类,这个注册类在合并的时候并不会被合并到代码内。
2、 transform的扫描完成之后,去生成好这个类的实现,这样就不会出现项目运行时的classNotFound异常了。
如果将注册类像ARouter一样放在基础库内部,我就要在编译的最后阶段去寻找那个包含有注册类的jar包,然后定位到那个类,对其进行修改。这要需要对所有jar包的进行扫描,这个过程相对来说是耗时的,而且我修改了整个jar包内的class,需要重新覆盖output的jar包。另外我也不需要像美团组件一样,用反射的方式去调用注册类,因为这个类会在最后编译时被生成和修改,而且类名,方法名和compileOnly的完全一样。
package com.zpw.emptyloader;
public class RouterRegistry {
public static void register() {
throw new NullPointerException("RouterRegistry Not Found. Use ClassLoader System");
}
}