主目录见:Android高级进阶知识(这是总目录索引)
我们在开发的时候为了提高效率往往会选择一个基于注解的框架,但是有时使用反射通常被认为是性能的收割机,所以我们会青睐编译期注解的使用,其实早在前面我们分析了[EventBus3.0源码解析]中我们就有看到,还有我们接下来要讲的ButterKnife也会用到,当然我今天要用来讲的例子[LRouter]这个项目也会使用这个。
一.目标
现在编译期注解这么火热,我们有理由要去学习一下,我们今天这篇文章是为了扫盲一下,使得我们下面分析这类型的框架更加得心应手。所以今天目标是:
1.自己能编写一个编译期注解项目;
2.在实际开发中能使用到这项技术;
3.能学习此类型的框架的源码,然后收为己用。
二.例子编写
在编写这个框架之前,我们需要一些准备,因为此类框架需要的模块有点多:
因为这是个完整的项目,我们只是挑出其中的编译期注解部分来讲:
- lrouter-annotation:用于放注解部分,是个java的模块
- lrouter-compiler:用于编写注解处理器,是个java模块
- lrouter-api:用于提供用户使用的api的,是个android模块
- app:这个是使用的实例,注解会在这里使用,也是android模块
当然了,目录不一定就是要分这么多个,大家可以根据自己的需要,有时候还可以进行合并。当然这些模块之间是有模块依赖的:
lrouter-compiler依赖lrouter-annotation模块
在使用这个注解和api的时候,我们也要添加一些依赖:
lrouter-api依赖lrouter-annotation,app依赖lrouter-api和lrouter-annotation
当然这里的lrouter-api在这里我们使用到的也不多,因为这是项目会使用到的api,具体使用可以查看github上面的说明。
1.注解模块lrouter-annotation实现
注解模块就是单纯地放一些注解,没有其他的东西:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Action {
String name();
String provider();
}
我们这里举其中一个注解来看看,因为我们是编译期的注解所以我们设置保留策略为CLASS即编译期注解(当然如果用反射的话Runtime或者还有一个source用的不多),并且说明我们注解是使用在类上面(还可以说明在Field或者Method等待上面),然后这里因为需要一个name和一个provider其中的值为String类型。
你如果需要几个注解就跟这个一样,只要设置上响应的Target和Retention即可。
2.注解处理器lrouter-compiler的实现
这块内容应该是编译期注解的核心了吧,不过放心,也不会太难,步骤是很固定的,在这里我们会使用一个auto-service库以及后面用来生成代码的javapoet:
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':lrouter-annotation')
}
auto-service可以帮我们去生成META-INF信息:
接着我们就可以编写注解处理器的核心代码了。
2.1.ProviderActionProcessor实现
要实现注解处理器我们要实现一个类继承AbstractProcessor:
@AutoService(Processor.class)
public class ProviderActionProcessor extends AbstractProcessor{
}
注解处理器里面包含几个重要的方法:
init()
初始化,得到Elements、Types、Filer等工具类
getSupportedAnnotationTypes()
描述注解处理器需要处理的注解
getSupportedSourceVersion()
处理器使用的java版本
process()
扫描分析注解,生成代码
首先我们会复写init()方法,该方法里面会有ProcessingEnvironment参数,我们可以根据这个来初始化一些辅助类:
private Filer mFileUtils;//跟文件相关的类,用于生成java源代码的
private Elements mElementUtils;//元素相关的类,可以理解为获取代码中的信息
private Messager mMessager;//是用来打印日志的跟Logger类似
我们重点要说一下Elements,他有几个子类,我们来说明一下:
public class ClassA { // TypeElement
private int var_0; // VariableElement
public ClassA() {} // ExecuteableElement
public void setA( // ExecuteableElement
int newA // TypeElement
) {
}
}
我们看到类为TypeElement,变量为VariableElement,方法为ExecuteableElement。到这里我们对Element有个简单的认知,然后我们继续重写getSupportedAnnotationTypes()方法:
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new LinkedHashSet<>();
annotationTypes.add(Action.class.getCanonicalName());
annotationTypes.add(Provider.class.getCanonicalName());
annotationTypes.add(Service.class.getCanonicalName());
annotationTypes.add(Application.class.getCanonicalName());
annotationTypes.add(IntentInterceptor.class.getCanonicalName());
annotationTypes.add(Interceptor.class.getCanonicalName());
return annotationTypes;
}
这里我们把我们支持的注解都添加进来,然后我们复写另外一个方法getSupportedSourceVersion():
@Override
public SourceVersion getSupportedSourceVersion(){
return SourceVersion.latestSupported();
}
这个里面我们直接返回我们处理器支持的最新的java版本。接着我们就来实现比较复杂部分的process方法。
2.2 process()实现
process方法比较复杂,因为我们主要的代码逻辑都在这里面,一般这个方法的步骤有两个:
1.收集信息
2.生成源代码
什么叫收集信息呢?就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的。
在这个例子中,我们会针对一个注解生成一个类,例如我们Action在app中使用的时候会生成一个MainAction$$Inject类:
然后我们来看怎样一步一步生成这个类的。
1)收集信息
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mProviderMap.clear();
mActionMap.clear();
mServiceMap.clear();
mApplicaitonMap.clear();
mIntentInterceptMap.clear();
mInterceptMap.clear();
if (!annotations.isEmpty()) {
Set<? extends Element> elesWithAction = roundEnv.getElementsAnnotatedWith(Action.class);
Set<? extends Element> elesWithProvider = roundEnv.getElementsAnnotatedWith(Provider.class);
Set<? extends Element> elesWithService = roundEnv.getElementsAnnotatedWith(Service.class);
Set<? extends Element> elesWithApplication = roundEnv.getElementsAnnotatedWith(Application.class);
Set<? extends Element> elesWithIntentInterceptor = roundEnv.getElementsAnnotatedWith(IntentInterceptor.class);
Set<? extends Element> elesWithInterceptor = roundEnv.getElementsAnnotatedWith(Interceptor.class);
...........
return true;
}
return false;
}
我们看到这里会获取到所有的使用Action等注解的元素集合,我们看到这里会先调用clear()方法清除掉map中的数据,因为process()方法有可能会调用多次,为了避免生成重复的源代码,我们这里做一下清理工作。获取到所有的注解的元素集合了我们就可以生成源代码了。
2)生成代码
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//省略收集信息代码
................
try {
generateProviderHelper(elesWithProvider);
generateActionHelper(elesWithAction);
generateServiceHelper(elesWithService);
generateApplicationHelper(elesWithApplication);
generateIntentInterceptorHelper(elesWithIntentInterceptor);
generateInterceptorHelper(elesWithInterceptor);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
return false;
}
我们看到代码会分别生成对应的注解的源代码,我们这里挑一个Action的生成代码方法来看:
private void generateActionHelper(Set<? extends Element> elesWithAction) throws Exception{
//遍历添加了Action注解的元素集合
for (Element element : elesWithAction){
//因为我们action是放在类上面的注解,所以我们这里会进行检查一下,而且这里类不能为private的
checkAnnotationValid(element, Action.class);
TypeElement classElement = (TypeElement) element;
//full class name
String actionClassName = classElement.getQualifiedName().toString();
ProxyInfo proxyInfo = mActionMap.get(actionClassName);
if (null == proxyInfo){
//构造proxyInfo对象,里面主要用于存放注解标注的类信息与生成类信息
proxyInfo = new ProxyInfo(mElementUtils, classElement);
mActionMap.put(actionClassName, proxyInfo);
}
//同时将action注解里面的name和provider取出设置给proxyinfo
Action actionAnnotation = classElement.getAnnotation(Action.class);
proxyInfo.setName(actionAnnotation.name());
proxyInfo.setProvider(actionAnnotation.provider());
}
//有了完整的信息之后就遍历出来生成源代码
for (String key : mActionMap.keySet()) {
ProxyInfo proxyInfo = mActionMap.get(key);
//这个是用javapoet生成的,这个是方法的生成
MethodSpec.Builder initBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.VOID);
//这个是方法里面的代码生成
initBuilder.addStatement("$T $N = $T.getInstance($T.getInstance()).findProvider($S)",
LRProviderClass,LRProviderClass.simpleName().toLowerCase(),LRouterClass,LRouterApplicationClass,proxyInfo.getProvider());
initBuilder.addCode("if(null != $N){\n",LRProviderClass.simpleName().toLowerCase());
initBuilder.addCode("$N.registerAction($S,new $T());\n}\n",
LRProviderClass.simpleName().toLowerCase(),proxyInfo.getName(),ClassName.get(proxyInfo.typeElement));
//这个是类的生成
TypeSpec actionInject = TypeSpec.classBuilder(proxyInfo.proxyClassName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(InjectorClass)
.addMethod(initBuilder.build())
.build();
JavaFile javaFile = JavaFile.builder(PACKAGE_NAME,actionInject).build();
javaFile.writeTo(mFileUtils);
}
}
我们看到代码比较长,其实逻辑不怎么难,我们都在关键地方有注释,代码其实就是获取到使用了注解的类的信息,然后再用javapoet来生成代码,关于[javapoet]我们这里不做详细的说明,有兴趣可以去这个github上面看看怎么使用。
3.注解在例子中的使用
我们前面编写了注解和注解生成器,这样我们就可以来使用这个注解来生成我们的源代码了,使用注解很简单,跟我们平常是一样的,我们来看下:
@Action(name = "main",provider = "main")
public class MainAction extends LRAction {//动作的执行
}
我们看到注解的使用非常简单,因为这个注解是用在类上面的,所以我们用法如上面所示,当然在编写这个之前我们还要配置一下我们的gradle文件。首先在工程目录下的gradle文件下添加:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
然后在当前模块下的gradle文件添加:
annotationProcessor project(':lrouter-compiler')
这样的话,我们只要编译一下工程,就会在当前模块的build——generated——source——apt——debug下生成我们的源代码了。
4.生成的源码的使用
生成完源码了,我们这里在lrouter-api中进行使用,我们这里会用到一点反射,跟大家理解的可能不大一样,不是说我们用到编译期注解就一点反射不会使用,这是不正确的,有可能会我们还是需要用到一点反射,有时我们会用到缓存来减少性能的消耗。我们现在看lrouter-api下的PackageScanner类:
public class PackageScanner {
/**
* 扫描
* */
public static List<InjectorPriorityWrapper> scan(Context ctx){
List<InjectorPriorityWrapper> clazzs = new ArrayList<>();
try{
PathClassLoader classLoader = (PathClassLoader) Thread
.currentThread().getContextClassLoader();
DexFile dex = new DexFile(ctx.getPackageResourcePath());
Enumeration<String> entries = dex.entries();
while (entries.hasMoreElements()) {
String entryName = entries.nextElement();
if (entryName.contains("com.lenovohit")){//过滤掉系统的类
Class<?> entryClass = Class.forName(entryName, false,classLoader);
if (entryName.contains("Provider$$Inject")){
clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.PROVIDER_PRIORITY,entryClass));
}else if (entryName.contains("Action$$Inject")){
clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.ACTION_PRIORITY,entryClass));
}else if(entryName.contains("$$Inject")){
((Injector)entryClass.newInstance()).inject();
}
}
}
//进行实例化
if (null != clazzs && clazzs.size() > 0){
Collections.sort(clazzs);
for (int i = 0; i < clazzs.size(); i ++){
InjectorPriorityWrapper clazz = clazzs.get(i);
((Injector)clazz.mClass.newInstance()).inject();
}
}
}catch (Exception e){
e.printStackTrace();
}
return clazzs;
}
}
这个类是得到dex文件下的信息,接着根据类名称反射生成这个类(这个类就是我们生成的源码),然后进行实例化调用。到这里我们编译期注解的例子已经讲解完毕了,其实步骤非常固定,只要操作一遍就熟悉了。如果文章写得有不懂的你可以下载源码下来看看,希望大家能在以后的项目中用到这个技术,还是非常方便。
总结:本文通过一个实际的例子来说明这项技术怎么使用,主要步骤包括:项目结构划分,注解模块实现,注解处理器模块实现,注解生成源码,生成的源码的使用等,希望大家能在学习本文完有个大的提高,因为你有能力去阅读这类型框架的源码了,而且能自己实现一套复杂的框架,希望本篇是你的第一步引导。