玩具路由组件

312 阅读5分钟

简单的说功能大部分和阿里美团的路由都半斤八两吧,以前公司拿来做组件化拆分的,支持编译时注册以及增量编译等等,整体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");
    }
}