感谢佬的点阅~~~
ARouter是一个用于Android App 进行组件化改造的框架 ,主要是为了支持模块间的路由、通信和解耦。其运用广、设计巧妙、涉及技术较全面,好吧,重点是新手友好。基本上每个androider都会研究它。当然,我也不例外,将其作为我步入android领域研究的第一个项目,回到正题👇👇👇
本篇文章主要由浅入深,由0到1的搭建一个仿ARouter的路由跳转框架,以实战角度认识ARouter以及学习掌握其所涉及的技术:apt(现在有ksp了,后面去学学)、自定义gradle插件技术、Transform(在gradle8.0+版本已被移除,本文实现了另一种方法)、ASM,以及运行这个项目需要去掌握的组件化以及android工程中模块配置等技术。其中有些技术网上资料太多了,就不在此论述了,会列一些学习过程中看的博客,至于本文主线还是ARouter的实现(非完全实现,为便于理解,好吧,想偷懒😷,有些地方没有按照原版来,不过大体框架上该有的基本上都实现了)。
0 前言
ARouter是个老生常谈的问题,其原理已经被各代androider扒的清楚的不能太清楚了,因此本文不再叙述其原理,重在实践,其原理可参见如下博客:
[1] “终于懂了” 系列:组件化框架 ARouter 完全解析(一) 原理详解 - 掘金
[2] “终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT技术 - 掘金
[3] “终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform/ASM - 掘金
所谓路由框架实质为对startActivity(intent)的包装,一个acitivity跳转另一个activity,如下:
val intent = Intent(this, FirstActivty::class.java)
startActivity(intent)
分析其需要什么,intent即为Intent,其需要当前的上下文以及目标页面类。如果只有几个页面手动输入一下这些还算轻松,但随着项目扩大,如果还手输,难免很繁琐。基于此,能否定义一个框架不用手动创建Intent并输入目标页面类。因此,自定义跳转框架的首要原则是减少手动创建Intent的步骤。
1 第一版(幼童版)
基于上述要求,仅减少Intent的重复书写,那么直接给每个目标页面类一个标志,然后使用Map让标志和类进行映射不就行了。
1.1 代码
class FakeARouter01 private constructor() {
private val groupMap = mutableMapOf<String, Class<*>>(
"firstActivity" to FirstActivity::class.java
)
fun navigation(path: String, context: Context) {
groupMap[path]?.let { destination ->
val intent = Intent(context, destination)
context.startActivity(intent)
Log.d("FakeARouter01", "跳转成功:$path")
} ?: run {
Log.e("FakeARouter01", "No activity found for path: $path")
}
}
companion object {
fun newInstance() = FakeARouter01()
}
}
1.2 小结
分析第一版代码,使用groupMap存映射关系,如果页面多了,也还需手动加入,似乎不是很优雅。有没有什么不需要手动加入groupMap的方法呢?
2 第二版(注解+反射)
基于上述问题,可引入注解,一为标明可跳转的activity,二为携带path。其次,标明了注解还需将标了注解的类识别以及加入groupMap,在此版中使用运行时反射获取。
2.1 代码
(1)Route注解
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Route(
val path: String
)
(2)获取带有Route注解的类,以及相应的path
此代码主要参考此篇文章:Android 获取apk中的所有类_android 获取所有类-CSDN博客,运行时获取到所有的类,从中挑出带有Route注解的类。
public class AntiPlugging {
private static BaseDexClassLoader mClassLoader = (BaseDexClassLoader) Thread.currentThread().getContextClassLoader();
public static void checkApp(Context context, Consumer<Map<String, Class<?>>> callback) {
if (mClassLoader == null) {
mClassLoader = (BaseDexClassLoader) context.getClassLoader();
}
new Thread(() -> {
Map<String, Class<?>> pathToClz = new HashMap<>();
Set<String> list = getAllClassesName(context.getPackageName()); // 获取除去库中所有的类的类名
List<Class<?>> routeAnnotationClasses = getRouteAnnotationClasses(list, Route.class); // 获取带有Route注解的类
if (!routeAnnotationClasses.isEmpty()) {
for (Class<?> clz : routeAnnotationClasses) {
Log.d("AntiPlugging anntotation", clz.getName());
String path = getRouteAnnotationPath(clz); // 获取path,与相应类形成映射对
if (path != null) {
pathToClz.put(path, clz);
}
}
}
if (callback != null) {
callback.accept(pathToClz);
}
}).start();
}
private static String getRouteAnnotationPath(Class<?> clazz) {
Route annotation = clazz.getAnnotation(Route.class);
String path = null;
if (annotation != null) {
path = annotation.path();
}
return path;
}
private static List<Class<?>> getRouteAnnotationClasses(Set<String> allClasses, Class<? extends Annotation> annotationClass) {
List<Class<?>> annotatedClasses = new ArrayList<>();
for (String className: allClasses) {
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(annotationClass)) {
annotatedClasses.add(clazz);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
return annotatedClasses;
}
private static Set<String> getAllClassesName(String targetPackageName) {
Set<String> classNames = new HashSet<>();
try {
Field f_pathList = getField("pathList", BaseDexClassLoader.class);
Field f_dexElements = getField("dexElements", getClass("dalvik.system.DexPathList"));
Field f_dexFile = getField("dexFile", getClass("dalvik.system.DexPathList$Element"));
Object pathList = getObjectFromField(f_pathList, mClassLoader);
Object[] list = (Object[]) getObjectFromField(f_dexElements, pathList);
String prefix = targetPackageName + '.';
if (list == null) {
return classNames;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
for (Object o: list) {
DexFile file = (DexFile) getObjectFromField(f_dexFile, o);
if (file != null) {
Enumeration<String> entries = file.entries();
while (entries != null && entries.hasMoreElements()) {
String className = entries.nextElement();
if (className.startsWith(prefix)) {
classNames.add(className);
}
}
}
}
} else {
Class<?> dexFile = getClass("dalvik.system.DexFile");
if (dexFile != null) {
Field f_mCookie = getField("mCookie", dexFile);
if (f_mCookie != null) {
Method classNameList = getMethod("getClassNameList", dexFile, Object.class);
if (classNameList != null) {
for (Object o : list) {
Object file = getObjectFromField(f_dexFile, o);
Object o_mCookie = getObjectFromField(f_mCookie, file);
if (o_mCookie == null) {
continue;
}
String[] classList = (String[]) classNameList.invoke(file, o_mCookie);
if (classList != null) {
for (String className: classList) {
if (className.startsWith(prefix)) {
classNames.add(className);
}
}
}
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return classNames;
}
private static Field getField(String field, Class<?> Class) throws NoSuchFieldException {
if (field != null && Class != null) {
return Class.getDeclaredField(field);
}
return null;
}
private static Method getMethod(String method, Class<?> Class, Class<?>... parameterTypes) throws NoSuchMethodException {
if (Class != null && parameterTypes != null) {
Method res = Class.getDeclaredMethod(method, parameterTypes);
res.setAccessible(true);
return res;
}
return null;
}
private static Class<?> getClass(String className) throws ClassNotFoundException {
return mClassLoader.loadClass(className);
}
private static Object getObjectFromField(Field field, Object arg) throws IllegalAccessException {
if (field != null && arg != null) {
field.setAccessible(true);
return field.get(arg);
}
return null;
}
}
(3)注入groupMap
class FakeARouter01 private constructor() {
fun init(context: Context) {
AntiPlugging.checkApp(context, Consumer { res ->
res.forEach { (key, value) ->
groupMap.putIfAbsent(key, value)
Log.d("FakeARouter01", "获取及注入:$key ${value.simpleName}")
}
} )
}
fun navigation(path: String, context: Context) {
groupMap[path]?.let { destination ->
val intent = Intent(context, destination)
context.startActivity(intent)
Log.d("FakeARouter01", "跳转成功:$path")
} ?: run {
Log.e("FakeARouter01", "No activity found for path: $path")
}
}
companion object {
private val groupMap = mutableMapOf<String, Class<*>>()
fun newInstance() = FakeARouter01()
}
}
运行程序,获取path与相应的类、注入group、跳转均可行:
2.2 小结
第二版在第一版的基础上更加省事了,但也还有不足:第一,在app运行启动时获取包中所有的类以及映射路径和相应的类,会严重增加app负担及其启动时间;第二,groupMap声明为静态变量(此种情况下也只能声明为静态变量),那么其在整个app进程存在过程中都会存在内存中,如果path较少没啥问题,但随着path增多,会占据很多内存。第三,为了不阻塞主线程,这里使用的是子线程解析所有类,如果解析的类太多,会很耗时,就会存在要跳转时groupMap中还不存在相关映射。在这里进行了实验,给子线程加了10秒sleep,确实出现了这种情况。
3 第三版(apt+javapoet)
针对第二版的问题,可以使用java自带的apt技术,即注解处理器,在编译期去处理相应的注解以及获取到相应的类。其次,获取到相应的类应该如何将其加载到内存,由于apt是编译时技术,因此此时加载没啥用。
此时就可以引入javapoet技术,生成一个帮助类函数,如下:
fun loadInto( atlas: Map<String, RouteMeta>){
atlas.put(key, RouteMeta.build(...))
}
在app启动时或者要跳转时去调用这个函数,就可将映射加载到内存中去,这样就可解决上述第二版中的三个问题。
3.1 代码
(1)路由元信息RouteMeta,存储路由的相关信息
在这里我就写的比较简单了,ARouter这里可写了很多东西,而且还将其包装,实现了一个postcard,不过在这里实现页面的跳转,如下几个也够了。
class RouteMeta public constructor(
var path: String,
var group: String,
var destination: Class<*>? = null,
var className: ClassName? = null
){
constructor(path: String, group: String, destination: Class<*>?) : this(path, group, destination, null)
constructor(path: String, group: String, className: ClassName?) : this(path, group, null, className)
companion object {
fun build(path: String, group: String, destination: Class<*>?) : RouteMeta {
return RouteMeta(path, group, destination)
}
}
}
(2)使用apt处理Route注解,同时使用javapoet生成加载映射关系的类:
ARouter在该processor中,还进行了参数的处理,因为跳转activity可以携带一些参数给另一个activity,本质上也是在生成的group帮助类函数中将参数以map的形式存储然后再注入RouteMeta中的。
@AutoService(Processor::class)
@SupportedAnnotationTypes("com.example.libarouter.annotation.Route")
class RouteProcessor : BaseProcessor() {
private val groupMap = mutableMapOf<String, MutableSet<RouteMeta>>() //存各模块带有Route注解的类信息
private val rootMap = mutableMapOf<String, String>() //存各模块的group类
override fun init(p0: ProcessingEnvironment?) {
super.init(p0)
}
override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment?
): Boolean {
println(">>> Route start process... <<<")
annotations?.takeIf { it.isNotEmpty() } ?.let {
val routeElements = roundEnv?.getElementsAnnotatedWith(Route::class.java)
try {
println("routeElements size: " + routeElements?.size)
parseRoute(routeElements)
} catch (e: Exception) {
println("Error: " + e.message)
}
return true
}
return false
}
private fun parseRoute(elements: Set<Element>?) {
elements?.takeIf { it.isNotEmpty() } ?.forEach { element ->
if (element.kind == ElementKind.CLASS) { //类-类型
val typeElement = element as TypeElement
val className = typeElement.simpleName.toString()
val packageName = processingEnv.elementUtils.getPackageOf(typeElement).toString()
val routeAnnotation = typeElement.getAnnotation(Route::class.java)
val routeMeta = RouteMeta(routeAnnotation.path, moduleName!!, ClassName.get(packageName, className))
groupMap.getOrPut(routeMeta.group) { mutableSetOf() } .add(routeMeta)
}
}
for ((groupName, groupData) in groupMap) {
generateGroupClass(groupName, groupData) //通过javapoet生成group类
}
generateRootClass() // 通过javapoet生成root类
}
/**生成如下类:
* public class ARouter$$Group$$moduleName implements IRouteGroup {
* @Override
* public void loadInto(Map<String, RouteMeta> atlas) {
* atlas.put(moduleName, RouteMeta.build(...));
* }
* }
*/
private fun generateGroupClass(groupName: String, groupData: Set<RouteMeta>) {
// 生成参数
val inputMapTypeOfGroup = ParameterizedTypeName.get(
ClassName.get(Map::class.java),
ClassName.get(String::class.java),
ClassName.get(RouteMeta::class.java)
)
val groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build()
// 生成loadInto方法
val loadIntoMethodBuilder = MethodSpec.methodBuilder("loadInto")
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
.returns(Void.TYPE)
.addParameter(groupParamSpec)
groupData.forEach { routeMeta ->
generateProviderClass(routeMeta) //通过javapoet生成provider类
loadIntoMethodBuilder.addStatement(
"atlas.put($S, $T.Companion.build($S, $S, $T.class))",
routeMeta.path,
ClassName.get(RouteMeta::class.java),
routeMeta.path,
routeMeta.group,
routeMeta.className
)
}
val groupClassName = "FakeARouter${SEPARATORS}Group${SEPARATORS}$moduleName"
val groupClass = TypeSpec.classBuilder(groupClassName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ClassName.get("com.example.libarouter.model", "IRouteGroup"))
.addMethod(loadIntoMethodBuilder.build())
.build()
rootMap[groupName] = groupClassName
val javaFile = JavaFile.builder(PACKAGE_OF_GENERATE_FILE, groupClass).build()
try {
javaFile.writeTo(processingEnv.filer)
} catch (e: Exception) {
println("error: ${e.message}")
}
}
/**生成如下类:
* public class FakeARouter$$Root$$moduleName implements IRouteRoot {
* @Override
* public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
* routes.put("moduleName", ARouter$$Group$$moduleName.class);
* }
* }
*/
private fun generateRootClass() {
val inputMapTypeOfRoot = ParameterizedTypeName.get(
ClassName.get(Map::class.java),
ClassName.get(String::class.java),
ParameterizedTypeName.get(
ClassName.get(Class::class.java),
WildcardTypeName.subtypeOf(ClassName.get("com.example.libarouter.model", "IRouteGroup"))
)
)
val rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build()
val loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder("loadInto")
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
.returns(Void.TYPE)
.addParameter(rootParamSpec)
rootMap.forEach { (moduleName, className) ->
loadIntoMethodOfRootBuilder.addStatement(
"routes.put($S, $T.class)",
moduleName,
ClassName.get(PACKAGE_OF_GENERATE_FILE, className)
)
}
val rootFileName = "FakeARouter${SEPARATORS}root${SEPARATORS}$moduleName"
val rootCLass = TypeSpec.classBuilder(rootFileName)
.addSuperinterface(ClassName.get("com.example.libarouter.model", "IRouteRootJava"))
.addModifiers(Modifier.PUBLIC)
.addMethod(loadIntoMethodOfRootBuilder.build())
.build()
val javaFile = JavaFile.builder(PACKAGE_OF_GENERATE_FILE, rootCLass).build()
try {
javaFile.writeTo(processingEnv.filer)
} catch (e: Exception) {
println("error: ${e.message}")
}
}
private fun generateProviderClass(routeMeta: RouteMeta) {
// 生成FakeARouter$$Providers$$moduleName类,模块对外的统一api接口
// 与上述两个生成方式差不多
// 由于我的demo目前用不到就不写啦
}
}
(2)FakeARouter对外api
代码:
class FakeARouter private constructor() {
// 就在这里就简单的用个参数存了
// 正版的使用了单独的一个存储类Warehouse
private var groupMap: MutableMap<String, RouteMeta> = mutableMapOf()
fun init(routeGroup: IRouteGroup) {
routeGroup.loadInto(groupMap) // 调用生成的函数将其注入groupMap中
}
companion object {
private class Holder {
companion object {
val INSTANCE = FakeARouter()
}
}
fun getInstance(): FakeARouter {
return Holder.INSTANCE
}
}
fun navigation(path: String, context: Context) {
groupMap[path]?.let { routeMeta ->
val intent = Intent(context, routeMeta.destination)
context.startActivity(intent)
Log.d("FakeARouter01", "跳转成功:$path")
} ?: run {
Log.e("FakeARouter01", "No activity found for path: $path")
}
}
}
(3)app启动加载以及跳转
// 加载
class FakeARouterApp : Application() {
override fun onCreate() {
super.onCreate()
FakeARouter.getInstance().init(`FakeARouter$$Group$$app`()) // 直接调用生成的类
}
}
// 跳转
@Route(path = "firstActivity")
class FirstActivity : Activity() {
private lateinit var activityFirstBinding: ActivityFirstBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityFirstBinding = ActivityFirstBinding.inflate(layoutInflater)
setContentView(activityFirstBinding.root)
activityFirstBinding.btn.setOnClickListener {
FakeARouter.getInstance().navigation("secondActivity", this)
}
}
}
(4)跳转成功
3.2 小结
至此基本上可用了,也基本上摆脱了手动注册等工作,但还有一个地方需要手动,即app启动时调用loadInto函数,将跳转映射加载入内存。
4 第四版(gradle+Transform)
为了做到极致的自动化,ARouter引入了Transform以及ASM技术。Transform扫描到要修改的类,再使用ASM修改class文件,进行代码注入。由于Transform技术是打包过程中的技术,因此实现这个需要自定义gradle插件,所以此版还需采用自定义gradle插件技术。
(1)自定义gradle-plugin
自定义gradle插件在此就不进行论述了,可参考以下资料,虽然在定义插件的时候也踩了一些坑:
[1] Android Gradle学习(四)- gradle插件开发和调试AOP思想 - 掘金
[2] Android 自定义Gradle插件(一): 创建自定义插件与发布自定义Gradle插件 - 掘金
[3] 如何使用Android Studio自定义Gradle插件(gradle7.0)Android Stuido 自定义 G - 掘金
⚠️:新建这个目录时不要一下子这样新建,虽然最后看起来是这样,但它不生效!【裂开】,需要先建META-INF目录,再建gradle-plugins目录。
⚠️:Android Stdio的gradle一般只显示test task,需要将如下配置打开,打开后gradle就有上面的所示的其他任务了:
2)transform被移除
进入代码编写,按理说一切应正常进行,可是出意外了,gradle 8.0及以上,API 'android.registerTransform' 被移除了【裂开】,此外官网说没有单一的替代API【裂开】:
[1] Transform API 废弃了,路由插件怎么办? - 掘金
(3)此demo实现的gradle-plugin目录如下:
- PluginLaunch主要为了启动插件
- ScanAndTrandformAllClassesTask是一个Task,继承自DefaultTask,实现了类似transform的功能,即扫描所有的class文件以及jar包,获取到第三版生成的帮助类以及LogisticsCenter类
- ScanSetting是一个数据存储类,主要为了存储实现了IRouteRoot、IInterceptorGroup、IProviderGroup这三个接口的类
- ScanUtils是扫描帮助类,识别帮助类以及LogisticsCenter类
- InjectUtils是ASM代码注入类,在LogisticsCenter类的loadRouterMap方法中注入如下代码:
register("com.example.fakearouter01.routes.FakeARouter$$root$$app");
4.1 代码
ScanSetting、ScanUtils、InjectUtils的代码分别对应原版arouter的ScanSetting、ScanUtil、RegisterCodeGenerator,代码差不多,为减少篇幅就不列出了。
(1)PluginLaunch
class PluginLaunch : Plugin<Project> {
override fun apply(project: Project) {
// 仅application模块用这个插件
if (project.plugins.hasPlugin(AppPlugin::class.java)) {
println("====start apply plugin====")
val androidComponents =
project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
val providerScanAndTransformAllClassesTask = project.tasks.register(
"${variant.name}ScanAndTransformAllClassesTask",
ScanAndTransformAllClassesTask::class.java
)
// 可以换成ScopedArtifacts.Scope.Project,提升编译速度
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(providerScanAndTransformAllClassesTask)
.toTransform(
ScopedArtifact.CLASSES,
ScanAndTransformAllClassesTask::allJars,
ScanAndTransformAllClassesTask::allDirectories,
ScanAndTransformAllClassesTask::output
)
}
}
}
}
(2)ScanAndTrandformAllClassesTask
abstract class ScanAndTransformAllClassesTask : DefaultTask() {
@get:InputFiles
abstract val allDirectories: ListProperty<Directory>
@get:InputFiles
abstract val allJars: ListProperty<RegularFile>
@get:OutputFile
abstract val output: RegularFileProperty
@TaskAction
fun taskAction() {
val leftSlash = File.separator == "/"
// 三种目标类的接口类:root、provider、interceptor
val targetList: List<ScanSetting> = listOf(
ScanSetting("IRouteRoot"),
ScanSetting("IInterceptorGroup"),
ScanSetting("IProviderGroup"),
)
JarOutputStream(output.asFile.get().outputStream()).use { jarOutput ->
// Scan directory
allDirectories.get().forEach { directory ->
val directoryPath =
if (directory.asFile.absolutePath.endsWith(File.separatorChar)) {
directory.asFile.absolutePath
} else {
directory.asFile.absolutePath + File.separatorChar
}
//println("Directory is $directoryPath")
directory.asFile.walk().forEach { file ->
if (file.isFile) {
val entryName = if (leftSlash) {
file.path.substringAfter(directoryPath)
} else {
file.path.substringAfter(directoryPath).replace(File.separatorChar, '/')
}
//println("\tDirectory entry name $entryName")
if (entryName.isNotEmpty()) {
if (entryName.startsWith("com/example/fakearouter01/routes/")) {
println("\tfakearouter routes name $entryName")
file.inputStream().use { input ->
ScanUtils.scanClass(input, targetList, false)
}
}
// 流只允许读,需Copy到另外的流
file.inputStream().use { input ->
jarOutput.saveEntry(entryName, input)
}
}
}
}
}
var originInject: ByteArray? = null
// 获取
val jars = allJars.get().map { it.asFile }
for (sourceJar in jars) {
//println("\tjar file is $sourceJar")
val jar = JarFile(sourceJar)
val entries = jar.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
try {
// Exclude directory
if (entry.isDirectory || entry.name.isEmpty()) {
continue
}
//println("\tJar entry is ${entry.name}")
if (entry.name != "com/example/fakearouter_api/LogisticsCenter.class") {
// Scan and choose
if (entry.name.startsWith("com/example/fakearouter01/routes/")) {
jar.getInputStream(entry).use { inputs ->
ScanUtils.scanClass(inputs, targetList, false)
}
}
// Copy
jar.getInputStream(entry).use { input ->
jarOutput.saveEntry(entry.name, input)
}
} else {
// Skip,在后面进行asm注入
println("Find inject byte code, Skip ${entry.name}")
jar.getInputStream(entry).use { inputs ->
originInject = inputs.readAllBytes()
println("Find before originInject is ${originInject?.size}")
}
}
} catch (e: Exception) {
// Format Optimize: exclude [java.util.zip.ZipException: duplicate entry: META-INF/MANIFEST.MF]
if(e is ZipException && e.message?.contains("META-INF/MANIFEST.MF") == true){
// Skip META-INF/MANIFEST.MF
}else{
println("[Warning] Merge [jar:entry] ${jar.name}:${entry.name}, error is $e ")
}
}
}
jar.close()
}
// Do inject,asm
println("Start inject byte code")
if (originInject == null) { // Check
error("Can not find ARouter inject point, Do you import ARouter?")
}
val resultByteArray = InjectUtils.referHackWhenInit( //注入函数
ByteArrayInputStream(originInject), targetList
)
// 保存到jar包
jarOutput.saveEntry(
"com/example/fakearouter_api/LogisticsCenter.class",
ByteArrayInputStream(resultByteArray)
)
println("Inject byte code successful")
}
}
private fun JarOutputStream.saveEntry(entryName: String, inputStream: InputStream) {
this.putNextEntry(JarEntry(entryName))
IOUtils.copy(inputStream, this)
this.closeEntry()
}
}
(3)LogisticsCenter
public class LogisticsCenter {
private static Context mContext;
private static boolean registerByPlugin;
public synchronized static void init(Context context) {
mContext = context;
Log.i("FakeARouter", "LogisticsCenter init");
loadRouterMap();
}
private static void loadRouterMap() {
registerByPlugin = false;
}
private static void register(String className) {
if (!TextUtils.isEmpty(className)) {
try {
if (registerByPlugin) {
Log.i("FakeARouter", "load arouter map by plugin");
} else {
// 正版这里加了缓存
Class<?> clazz = Class.forName(className);
Log.d("FakeARouter", "LogisticsCenter register" + className);
Object obj = clazz.getConstructor().newInstance();
if (obj instanceof IRouteRoot) {
registerRouteRoot((IRouteRoot) obj);
}
}
} catch (Exception e) {
throw (RuntimeException) e;
}
}
}
private static void registerRouteRoot(IRouteRoot routeRoot) {
markRegisteredByPlugin();
if (routeRoot != null) {
routeRoot.loadInto(WareHouse.INSTANCE.getGroupIndex());
}
}
private static void markRegisteredByPlugin() {
if (!registerByPlugin) {
registerByPlugin = true;
}
}
public static void completion(RouteMeta routeMeta) {
doCompletion(routeMeta);
}
public static void doCompletion(RouteMeta routeMeta) {
RouteMeta completionRouteMeta = WareHouse.INSTANCE.getRoutes().get(routeMeta.getPath());
if (completionRouteMeta == null) {
Class<? extends IRouteGroup> groupMeta = WareHouse.INSTANCE.getGroupIndex().get(routeMeta.getGroup());
try {
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
Log.i("FakeARouter", "LogisticsCenter doCompletion: " + iGroupInstance.getClass().getName());
iGroupInstance.loadInto(WareHouse.INSTANCE.getRoutes());
} catch (Exception e) {
throw (RuntimeException) e;
}
doCompletion(routeMeta);
} else {
routeMeta.setDestination(completionRouteMeta.getDestination());
}
}
}
(4)FakeARouter-api
class FakeARouter private constructor() {
private var groupMap: MutableMap<String, RouteMeta> = mutableMapOf()
fun init(routeGroup: IRouteGroup) {
//routeGroup.loadInto(groupMap)
}
fun init(application: Application) {
LogisticsCenter.init(application)
}
companion object {
private class Holder {
companion object {
val INSTANCE = FakeARouter()
}
}
fun getInstance(): FakeARouter {
return Holder.INSTANCE
}
}
fun navigation(path: String, context: Context) {
val routeMeta = RouteMeta.build(path, "app", null)
LogisticsCenter.completion(routeMeta)
val intent = Intent(context, routeMeta.destination)
context.startActivity(intent)
Log.d("FakeARouter", "跳转成功:$path")
}
}
(5)注入结果
⚠️:用kotlin写某些类有些问题,比如IRouteRoot(其实也行,但比较麻烦)、LogisticsCenter这两个类,运行正常,但功能实现不了,后面反编译出来一看,代码不对劲,所以后面就换成java编写了
4.2 小结
至于此,用于路由的话,基本上就可行了,不需要手动去写Intent等操作了。但相比ARouter还有些粗糙。ARouter考虑的比较周全,其还实现了在运行时需要了再加载映射进来,还加入了缓存、路由拦截,考虑了线程安全等问题。由于本文主要为了熟悉其中涉及的技术,实现这些基本上把这些技术都用了一遍,至于ARouter的其他部分目前以理解为主,实现的话后续再去完善。
5 总结
本文主要运用apt、gradle-plugin、ASM等技术,以一个初学者的角度,基于ARouter思考一个路由框架由0到1的实现过程,以及使用自定义Task解决gradle 8.0及以上,API 'android.registerTransform' 被移除的问题。
完结,撒花🎉🎉🎉。
写点感想:前前后后断断续续历时好几个月,终于实现了。最大的困难不是写代码,而是调环境,引入工程,一调一个不吱声。自以为操作无误,实际上也是根据资料一步步来的,可是在运行时就是有问题【😣】。但是,放弃吧,又不甘心,不放弃吧,头发不保【拿捏🫴】。
可谓是:代码虐我千百遍,我待代码如初恋呀