注册

安卓进阶二: 这次我把ARouter源码搞清楚啦!

四. ARouter 注解处理器:arouter-compiler

ARouter 生成路由信息代码利用了注解处理器的特性。 arouter-compiler 就是注解处理代码模块,先看看该模块的依赖库

//定义的注解类,以及相关数据实体类
implementation 'com.alibaba:arouter-annotation:1.0.6'

annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
compileOnly 'com.google.auto.service:auto-service-annotations:1.0-rc7'

implementation 'com.squareup:javapoet:1.8.0'

implementation 'org.apache.commons:commons-lang3:3.5'
implementation 'org.apache.commons:commons-collections4:4.1'

implementation 'com.alibaba:fastjson:1.2.69'

依赖库中注解处理相关依赖库说明:

  • Auto-service官方文档 针对被@AutoService注解的类,生成对应元数据,在javac 编译的时候,会自动加载,并放在注释处理环境中。
  • javapoet :square推出的开源java代码生成框架,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。
  • arouter-annotation :arouter 的注解类们和路由信息实体类们
  • 其他,工具类库

RouteProcessor注解处理器处理流程说明

我们先看看路由处理器 RouteProcessor

@AutoService(Processor.class)
@SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED})
public class RouteProcessor extends BaseProcessor {
@Override
//在该方法中可以获取到processingEnvironment对象,
//借由该对象可以获取到生成代码的文件对象, debug输出对象,以及一些相关工具类
public synchronized void init(ProcessingEnvironment processingEnv) {
//...
super.init(processingEnv);
}
@Override
//返回所支持的java版本,一般返回当前所支持的最新java版本即可
public SourceVersion getSupportedSourceVersion() {
//...
return super.getSupportedSourceVersion();
}

@Override
//必须实现 扫描所有被注解的元素,并作处理,最后生成文件。该方法的返回值为boolean类型,若返回true,
//则代表本次处理的注解已经都被处理,不希望下一个注解处理器继续处理,
//否则下一个注解处理器会继续处理。
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//...
return false;
}

}

可以看到处理注解主要是在process方法。 RouteProcessor 继承 BaseProcessor 间接继承了AbstractProcessor,在BaseProcessor#init 方法中,获取到processingEnv 中的各种实用工具,以供处理注解使用。 值得一提的是,init 中获取了moduleName 和 generateDoc 参数代码如下:

if (MapUtils.isNotEmpty(options)) {
///AROUTER_MODULE_NAME
moduleName = options.get(KEY_MODULE_NAME);
///AROUTER_GENERATE_DOC
generateDoc = VALUE_ENABLE.equals(options.get(KEY_GENERATE_DOC_NAME));
}

这一块就是我们常常需要在gradle中配置的arguments 的由来:

android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
}
}
}
}
//或者kotlin
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}

接下来看RouteProcessor#process方法的具体实现:

Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
this.parseRoutes(routeElements);

代码中拿到了所有标注@Route注解的相关类元素。 然后在parseRoutes方法中进行处理: 省略了一大把代码后,代码还是很长,可以直接到下面看结论:

private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
if (CollectionUtils.isNotEmpty(routeElements)) {
// prepare the type an so on.
logger.info(">>> Found routes, size is " + routeElements.size() + " <<<");

rootMap.clear();
///省略类型获取代码

/*...省略构建'loadInto'方法描述,通过定义变量名,
定义类型最后得出 MethodSpec.Builder
void loadInto(Map<String, Class<? extends IRouteGroup>> atlas);
*/

MethodSpec.Builder loadIntoMethodOfRootBuilder;

// Follow a sequence, find out metas of group first, generate java file, then statistics them as root.
for (Element element : routeElements) {
//..省略相关代码,根据element类型,创建出对应的RouteMate实例,得到路由信息,
//并且通过injectParamCollector 方法将Activity和Fragmentr内部的所有@AutoWired
//注解 的信息放到MetaData的 paramsType 和injectConfig 中
v
//对 routeMate进行分类,在groupMap中填充对应数据
categories(routeMeta);
}

/*...省略构建'loadInto'方法描述,通过定义变量名,
定义类型最后得出 MethodSpec.Builder,主要用来构建providers索引。
void loadInto(Map<String, RouteMeta> providers);
*/

MethodSpec.Builder loadIntoMethodOfProviderBuilder;

Map<String, List<RouteDoc>> docSource = new HashMap<>();

//...
if (MapUtils.isNotEmpty(rootMap)) {
// Generate root meta by group name, it must be generated before root, then I can find out the class of group.
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
}
}

// 2.Output route doc 写入json到doc文档中
if (generateDoc) {
docWriter.append(JSON.toJSONString(docSource, SerializerFeature.PrettyFormat));
docWriter.flush();
docWriter.close();
}

// Write provider into disk
//3.生成对应的IProviderGroup 类代码文件 ARouter$$Providers$$[moduleName]
String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(providerMapFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IProviderGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfProviderBuilder.build())
.build()
).build().writeTo(mFiler);

logger.info(">>> Generated provider map, name is " + providerMapFileName + " <<<");

// Write root meta into disk.
//4. 生成对应的IRouteRoot 类代码文件 ARouter$$Root$$[moduleName]
String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(rootFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(elementUtils.getTypeElement(ITROUTE_ROOT)))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfRootBuilder.build())
.build()
).build().writeTo(mFiler);

logger.info(">>> Generated root, name is " + rootFileName + " <<<");
}
}

代码很长,关键结果就是三点,

  1. com.alibaba.android.arouter.routes 包名下生成ARouter$$Group$$[GroupName] 类,包含路由组的所有路由信息。
  2. 在该包名下生成ARouter$$Root$$[moduleName]类,包含所有组的信息。
  3. 在该包名下生成ARouter$$Providers$$[moduleName] 类,包含所有Providers索引
  4. 在docs下,生成文件名为 "arouter-map-" + moduleName + ".json" 的文档。

其他注解处理器说明

剩下还有两个注解处理器 InterceptorProcessor 和 AutowiredProcessor。 生成代码逻辑大同小异,只是逻辑复杂度的区别,

  • AutowiredProcessor :处理@Autowired注解的参数,以参数所在对应的类分类,生成[classSimpleName]$$ARouter$$Autowired 代码文件,以在Activity或者Fragment跳转的时候自动从intent中获取数据,并对activity 和 fragment 对象赋值。
  • InterceptorProcessor: 处理@Interceptor注解。生成对应的ARouter$$Interceptors$$[modulename]代码文件,提供拦截器功能。

值得一提的是,对于自定义类型的@AutoWiredARouter提供了 SerializationService进行自定义,用户只需要实现该解析类就行。

小结

这个模块完成了之前ARouter初始化所需要的所有代码的生成。 ARouter 源码和源码的分析到这里,已经成功走到了闭环,主要功能都已经清楚了。 之前没有写过AnotationProcessor相关的代码生成库。这次算是学习到了整个注解处理代码生成框架的使用方式。也了解了ARouter 代码生成的原理和方式。 业余时间自己也尝试写一个简单的代码生成功能试试看吧。下面一小节,再看看ARouter初始化注册的可选方案,arouter-register的源码。

五. ARouter 自动注册插件:arouter-register

代码在arouter-gradle-plugin 文件夹下面,

刚开始查看这个模块的源码,部分代码老是飘红,找不到部分类,于是我修改了该模块build.gradle 中的gradle依赖版本号。从2.1.3 改成了 4.1.3。代码果然就正常了。 gradle插件调试可以更好地理解代码,参考网上的博客启动插件调试。

注册转换器

ARouter-register 插件通过 registerTransform api。添加了一个自定义Transform,对dex进行自定义处理。 直接看 该源码中的入口代码 PluginLaunch#apply

def isApp = project.plugins.hasPlugin(AppPlugin)
//only application module needs this plugin to generate register code
if (isApp) {
def android = project.extensions.getByType(AppExtension)
def transformImpl = new RegisterTransform(project)
android.registerTransform(transformImpl)
}

代码中调用了AppExtension.registerTransform方法注册了 RegisterTransform。查阅api文档可知,该方法的功能是:允许第三方方插件在将编译的类文件转换为 dex 文件之前对其进行操作。 那就知道了,该方法就是类文件转换中间的一道工序。

扫描class文件和jar文件,保存路由类信息

那工序做了什么呢?看看代码RegisterTransform#transform

@Override
void transform(Context context, Collection<TransformInput> inputs
, Collection<TransformInput> referencedInputs
, TransformOutputProvider outputProvider
, boolean isIncremental) throws IOException, TransformException, InterruptedException {

Logger.i('Start scan register info in jar file.')

long startTime = System.currentTimeMillis()
boolean leftSlash = File.separator == '/'

inputs.each { TransformInput input ->

//通过AMS 的 ClassVisistor 扫描所有的jar 文件,将所有扫描到的IRouteRoot IInterceptorGroup IInterceptorGroup类
//都加到ScanSetting 的 classList中
//详情可以看看 ScanClassVisitor
//如果jar包是 LogisticsCenter.class,标记该类文件到 fileContainsInitClass

input.jarInputs.each { JarInput jarInput ->
//排除对于support库,以及m2repository 内第三方库的扫描。scan jar file to find classes
if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
//扫描
ScanUtil.scanJar(src, dest)
}
//..省略重命名扫描过的jar包相关代码
}
// scan class files
//..省略扫描class文件相关代码,方式类似扫描jar包
}

Logger.i('Scan finish, current cost time ' + (System.currentTimeMillis() - startTime) + "ms")
//如果存在 LogisticsCenter.class 类文件
//插入注册代码到 LogisticsCenter.class 中
if (fileContainsInitClass) {
registerList.each { ext ->
//...省略一些判空和日志代码
///插入初始化代码
RegisterCodeGenerator.insertInitCodeTo(ext)
}
}
Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() - startTime) + "ms")
}

从代码中可知,这一块代码有四个关键点。

  1. 通过ASM扫描了对应的jar 文件和class文件,并将扫描到的对应routes包下的类加入到ScanSetting 的classList属性 中
  2. 如果扫描到包含LogisticsCenter.class 类文件,将该文件记录到fileContainsInitClass 字段中。
  3. 扫描完成的文件重命名。
  4. 最后通过RegisterCodeGenerator.``*insertInitCodeTo*``(ext) 方法插入初始化代码到LogisticsCenter.class中。

明白了扫描流程,我们再看看代码是怎么插入的.

遍历包含入口class的jar文件,准备插入代码

RegisterCodeGenerator.``*insertInitCodeTo*``(ext)代码中,先判断ScanSetting#classList是否为空,再判断文件是否是jar文件。如果判断都过了,最后走到 RegisterCodeGenerator#insertInitCodeIntoJarFile代码:

private File insertInitCodeIntoJarFile(File jarFile) {
//将包含 LogisticsCenter.class 的 jar文件,插入初始化代码
//操作在 ***.jar.opt 临时文件做
if (jarFile) {
def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
if (optJar.exists())
optJar.delete()
///通过JarFile 和JarEntry
def file = new JarFile(jarFile)
Enumeration enumeration = file.entries()
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
//遍历jar中的所有class,查询修改
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = file.getInputStream(jarEntry)
jarOutputStream.putNextEntry(zipEntry)
///如果是LogisticsCenter.class文件,调用referHackWhenInit 插入代码
///如果不是,不改变数据直接写入
if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
Logger.i('Insert init code to class >> ' + entryName)
//!!!!重点代码,插入初始化代码
def bytes = referHackWhenInit(inputStream)
jarOutputStream.write(bytes)
} else {
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
inputStream.close()
jarOutputStream.closeEntry()
}
jarOutputStream.close()
file.close()

if (jarFile.exists()) {
jarFile.delete()
}
optJar.renameTo(jarFile)
}
return jarFile
}

从代码中可知,按步骤梳理:

  1. 创建临时文件,***.jar.opt
  2. 通过输入输出流,遍历jar文件下面的所有class,判断是否LogisticCenter.class
  3. LogisticCenter.class 调用 referHackWhenInit 方法插入初始化代码,写入到opt临时文件
  4. 对其他class 原封不动写入opt临时文件
  5. 删除原来的jar文件,将临时文件改名为原来的jar文件名

这一步完成了对于jar文件的修改。插入了ARouter的自动注册初始化代码。

插入初始化代码

插入操作主要是找到 LogisticCenter 关键的插入代码在于RegisterCodeGenerator#referHackWhenInit

private byte[] referHackWhenInit(InputStream inputStream) {
ClassReader cr = new ClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, 0)
ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
return cw.toByteArray()
}

可以看到代码中利用了ams 框架的 ClassVisitor 来访问入口类。 再看MyClassVisistor 的visitMethod 实现:

@Override
MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
//generate code into this method
//针对loadRouterMap 方法进行处理
if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
}
return mv
}

可以看到,当asm访问的方法名为loadRouterMap时候,就通过RouteMethodVisitor 对齐进行操作,具体代码如下:

class RouteMethodVisitor extends MethodVisitor {
RouteMethodVisitor(int api, MethodVisitor mv) {
super(api, mv)
}
@Override
void visitInsn(int opcode) {
//generate code before return
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
extension.classList.each { name ->
name = name.replaceAll("/", ".")
mv.visitLdcInsn(name)//类名
// generate invoke register method into LogisticsCenter.loadRouterMap()
mv.visitMethodInsn(Opcodes.INVOKESTATIC
, ScanSetting.GENERATE_TO_CLASS_NAME
, ScanSetting.REGISTER_METHOD_NAME
, "(Ljava/lang/String;)V"
, false)
}
}
super.visitInsn(opcode)
}
@Override
void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals)
}
}

代码中涉及到asm MethodVisistor的不少api,我查询,简单了解下,博客链接在此 解释一下用的的几个方法

  • visitLdcInsn:访问ldc指令,向栈中压入参数
  • visitMethodInsn:调用方法的指令,上面代码中,用来调用LogisticsCenter.register(String className)方法
  • visitMaxs: 用以确定类方法在执行时候的堆栈大小。

小结

对这里我们就十分清晰了插入初始化代码的路径。

  1. 首先是扫描所有的jarclass,找到对应的routes包名的类文件和 包含 LogisticsCenter.class 类的jar文件。类文件名依据类别存放在ScanSetting中。
  2. 找到LogisticsCenter.class ,对他进行字节码操作,插入初始化代码。

整个register插件的流程就完成了

六. ARouter idea 插件:arouter helper

该插件源码在 arouter-idea-plugin 文件夹下面

刚开始的时候编译老不成功,于是我修改了源码模块中 id "org.jetbrains.intellij" 插件的版本号,从0.3.12 改成了 0.7.3,果然就可以成功运行编译了。命令./gradlew :arouter-idea-plugin:buildPlugin 可以编译插件。

插件效果

先看看这个用法效果。 安装很简单,只需要在插件市场搜索ARouter Helper 安装就行。 在安装了该插件之后,相关ARouter.build()路由的java代码那一行,行号右侧会出现一个定位图标,如下图所示。 img 点击定位图标,就能自动跳转到路由的定义类。 img

看完效果,我们直接看源码。 插件模块代码就一个类 com.alibaba.android.arouter.idea.extensions.NavigationLineMarker

判断是否是ARouter.build

NavigationLineMarker` 继承了`LineMarkerProviderDescriptor`,实现了`GutterIconNavigationHandler<PsiElement>
  • LineMarkerProviderDescriptor:将小图标(16x16 或更小)显示为行标记。也就是该插件识别到navigation方法之后,显示在行号右侧的标准图标。
  • GutterIconNavigationHandler 图标点击处理器,处理图标点击事件。

看看行图标的获取代码:

override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
return if (isNavigationCall(element)) {
LineMarkerInfo<PsiElement>(element, element.textRange, navigationOnIcon,
Pass.UPDATE_ALL, null, this,
GutterIconRenderer.Alignment.LEFT)
} else {
null
}
}
  1. 先是过isNavigationCall判断是否是ARouter.build() 方法。
  2. 然后配置LineMarkerInfo ,将this 配置为点击处理者

所以我们先看isNavigationCall

private fun isNavigationCall(psiElement: PsiElement): Boolean {
if (psiElement is PsiCallExpression) {
///resolveMethod:解析对被调用方法的引用并返回该方法。如果解析失败,则为 null。
val method = psiElement.resolveMethod() ?: return false
val parent = method.parent

if (method.name == "build" && parent is PsiClass) {
if (isClassOfARouter(parent)) {
return true
}
}
}
return false
}

该方法判断是否调用的是ARouter.build方法,如果是就返回true。展示行标记图标。

点击定位图标跳转源码

接下来再看点击图标的相关跳转 navigate方法:

override fun navigate(e: MouseEvent?, psiElement: PsiElement?) {
if (psiElement is PsiMethodCallExpression) {
///build方法参数列表
val psiExpressionList = (psiElement as PsiMethodCallExpressionImpl).argumentList
if (psiExpressionList.expressions.size == 1) {
// Support `build(path)` only now.
///搜索所有带 @Route 注解的类,匹配注解的path路径有没有包含路径参数,包含的话就跳转
val targetPath = psiExpressionList.expressions[0].text.replace("\"", "")
val fullScope = GlobalSearchScope.allScope(psiElement.project)
val routeAnnotationWrapper = AnnotatedMembersSearch.search(getAnnotationWrapper(psiElement, fullScope)
?: return, fullScope).findAll()
val target = routeAnnotationWrapper.find {
it.modifierList?.annotations?.map { it.findAttributeValue("path")?.text?.replace("\"", "") }?.contains(targetPath)
?: false
}

if (null != target) {
// Redirect to target.
NavigationItem::class.java.cast(target).navigate(true)
return
}
}
}

notifyNotFound()
}
  1. 获取build方法的参数,作为目标路径
  2. 搜索所有带 @Route 注解的类,匹配注解的path路径有没有包含目标路径参数
  3. 找到的目标文件直接跳转 NavigationItem::class.``*java*``.cast(target).navigate(true)

0 个评论

要回复文章请先登录注册