安卓进阶二: 这次我把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 + " <<<");
}
}
代码很长,关键结果就是三点,
- 在
com.alibaba.android.arouter.routes
包名下生成ARouter$$Group$$[GroupName]
类,包含路由组的所有路由信息。 - 在该包名下生成
ARouter$$Root$$[moduleName]
类,包含所有组的信息。 - 在该包名下生成
ARouter$$Providers$$[moduleName]
类,包含所有Providers
索引 - 在docs下,生成文件名为
"arouter-map-" + moduleName + ".json"
的文档。
其他注解处理器说明
剩下还有两个注解处理器 InterceptorProcessor
和 AutowiredProcessor
。 生成代码逻辑大同小异,只是逻辑复杂度的区别,
AutowiredProcessor
:处理@Autowired
注解的参数,以参数所在对应的类分类,生成[classSimpleName]$$ARouter$$Autowired
代码文件,以在Activity或者Fragment跳转的时候自动从intent
中获取数据,并对activity 和 fragment 对象赋值。InterceptorProcessor
: 处理@Interceptor
注解。生成对应的ARouter$$Interceptors$$[modulename]
代码文件,提供拦截器功能。
值得一提的是,对于自定义类型的@AutoWired
,ARouter
提供了 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")
}
从代码中可知,这一块代码有四个关键点。
- 通过ASM扫描了对应的jar 文件和class文件,并将扫描到的对应
routes
包下的类加入到ScanSetting
的classList
属性 中 - 如果扫描到包含
LogisticsCenter.class
类文件,将该文件记录到fileContainsInitClass
字段中。 - 扫描完成的文件重命名。
- 最后通过
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
}
从代码中可知,按步骤梳理:
- 创建临时文件,
***.jar.opt
- 通过输入输出流,遍历
jar
文件下面的所有class
,判断是否LogisticCenter.class
- 对
LogisticCenter.class
调用referHackWhenInit
方法插入初始化代码,写入到opt临时文件 - 对其他
class
原封不动写入opt
临时文件 - 删除原来的
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: 用以确定类方法在执行时候的堆栈大小。
小结
对这里我们就十分清晰了插入初始化代码的路径。
- 首先是扫描所有的
jar
和class
,找到对应的routes
包名的类文件和 包含LogisticsCenter.class
类的jar
文件。类文件名依据类别存放在ScanSetting中。 - 找到
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
代码那一行,行号右侧会出现一个定位图标,如下图所示。 点击定位图标,就能自动跳转到路由的定义类。
看完效果,我们直接看源码。 插件模块代码就一个类 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
}
}
- 先是过
isNavigationCall
判断是否是ARouter.build()
方法。 - 然后配置
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()
}
- 获取build方法的参数,作为目标路径
- 搜索所有带 @Route 注解的类,匹配注解的path路径有没有包含目标路径参数
- 找到的目标文件直接跳转
NavigationItem::class.``*java*``.cast(target).navigate(true)