作者:的一幕链接:https://www.jianshu.com/p/604094bf763e声明:本文已获
的一幕授权发表,转发等请联系原作者授权
说到java的apt技术,其实已经算不是很陌生了,在以前阅读第三方框架butterknife、Dagger2等框架的时候,看到过apt的影子。他是squareup公司出的javapoet技术,通过在java的编译时期生成类,提高了在运行时期通过反射调用的效率。大家试想一下,如果butterknife所有的注解在运行时期都通过反射调用相应的findViewById的话,那得多慢啊。所以可以看到butterknife都是通过apt技术来生成相应的_ViewBinding,大家可以看下app-->build-->generated-->source-->apt下面找到对应的_ViewBinding。好了废话不多说,咋们下面来直接来撸码。
实现功能还是跟butterknife框架findViewById的功能一样,经过前几篇的学习反射,注解所以才有今天的apt技术代码,所以不熟悉反射跟注解的伙伴们,还是先看下反射和注解如何使用。
-
android studio中创建一个java library的module,这里我起名字叫binder_annotation,专门用来放注解的,这里生成后是这个样子:
-
接着在刚创建的binder_annotation中创建注解
//在编译期起作用的注解@Retention(RetentionPolicy.CLASS)@Target(ElementType.FIELD)public @interface BindView { int value();}
-
创建java library的module,这里我起名字叫binder_compiler。先创建了后面再说
-
在app的module的build.gradle通过annotationProcessor添加依赖:
dependencies { .... annotationProcessor project(':binder_compiler') implementation project(':binder_annotation')}
这里注意了,在gradle tool>=2.2之后直接用annotationProcessor添加apt的依赖,如果是在gradle tool<2.2首先得在project添加:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
然后在app的build.gradle添加:
apply plugin: 'com.neenbedankt.android-apt'
添加依赖的地方:
apt project(':binder_compiler')
此处我用的是gradle tools 3.4.1因此直接用annotationProcessor添加依赖。
说完了整体的架子,下面来到binder_compiler下面,我们创建BinderProcessor类,并且继承于AbstractProcessor,该类是在编译期会进行类扫描的处理类。咋们需要实现,在实现之前需要了解几个方法:
//该方法指定类处理器是什么java版本,一般返回SourceVersion.latestSupported()表示最新的java版本就行@Overridepublic SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported();}
//指明有哪些注解需要被扫描到,返回注解的路径@Overridepublic Set<String> getSupportedAnnotationTypes() { //大部分class而已getName、getCanonicalNam这两个方法没有什么不同的。 //但是对于array或内部类等就不一样了。 //getName返回的是[[Ljava.lang.String之类的表现形式, //getCanonicalName返回的就是跟我们声明类似的形式。 HashSet<String> supportTypes = new LinkedHashSet<>(); supportTypes.add(BindView.class.getCanonicalName()); return supportTypes; //因为兼容的原因,特别是针对Android平台,建议使用重载getSupportedAnnotationTypes()方法替代默认使用注解实现}
在初始化的时候获取到扫描对象:
@Overridepublic synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); //processingEnvironment.getElementUtils(); 处理Element的工具类,用于获取程序的元素,例如包、类、方法。 //processingEnvironment.getTypeUtils(); 处理TypeMirror的工具类,用于取类信息 //processingEnvironment.getFiler(); 文件工具 //processingEnvironment.getMessager(); 错误处理工具 //初始化的时候获取到当前扫描的对象 //processingEnv是父类定义的ProcessingEnvironment对象,其实就是init方法回传过来的 mElementUtils = processingEnv.getElementUtils();}
我们的重头戏来了process方法:
@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //扫描整个工程 找出含有BindView注解的元素 //找到所有带有BindView注解的类,生成对应的****_ViewBinding类 Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class); //遍历元素 for (Element element : elements) { //BindView限定了只能属性使用,这里强转为VariableElement,如果是在类上面的,那么就是typeElement VariableElement variableElement = (VariableElement) element; //返回此元素直接封装(非严格意义上)的元素。 //类或接口被认为用于封装它直接声明的字段、方法、构造方法和成员类型 //这里就是获取封装属性元素的类元素 TypeElement classElement = (TypeElement) variableElement.getEnclosingElement(); //获取简单类名 String fullClassName = classElement.getQualifiedName().toString(); //里面放的是BinderClassCreator,关键生成***_ViewBinding类在里面生成的 BinderClassCreator creator = mCreatorMap.get(fullClassName); if (creator == null) { creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement), classElement); //生成之后就放到map中,方便下次使用 mCreatorMap.put(fullClassName, creator); } //获取元素注解 BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //注解值 int id = bindAnnotation.value(); creator.putElement(id, variableElement); } for (String key : mCreatorMap.keySet()) { BinderClassCreator binderClassCreator = mCreatorMap.get(key); //通过javapoet构建生成Java类文件 //第一个参数传入包名 //第二个参数传入TypeSpec JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName(), binderClassCreator.generateJavaCode()).build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } return false;}
process方法里面主要做了几件事:
-
扫描工程里面带有BindView注解的类
-
通过注解的类拿到类信息,生成BinderClassCreator对象,然后放到map中,将id和VariableElement对象给BinderClassCreator对象,后面会用到
-
最后生成javaFile对象,通过writeTo生成对应的***_ViewBinding。
在上面代码中将每一个要生成***_ViewBinding类的工作都交给了BinderClassCreator类,其实最关心的还是该类:首先看构造方法:
public static final String ParamName = "view";private TypeElement mTypeElement;private String mPackageName;private String mBinderClassName;//key是view在xml中的id,value是作用在类上面的element对象private Map<Integer, VariableElement> mVariableElements = new HashMap<>();/** * @param packageElement 包元素 * @param classElement 类元素 */public BinderClassCreator(PackageElement packageElement, TypeElement classElement) { this.mTypeElement = classElement; mPackageName = packageElement.getQualifiedName().toString(); mBinderClassName = classElement.getSimpleName().toString() + "_ViewBinding";}
构造器基本没做什么,主要是初始化包名和class类名。
存储了id和作用在类上面的element对象public void putElement(int id, VariableElement variableElement) { mVariableElements.put(id, variableElement);}
//生成类的代码public TypeSpec generateJavaCode() { return TypeSpec.classBuilder(mBinderClassName) //public 修饰类 .addModifiers(Modifier.PUBLIC) //添加类的方法 .addMethod(generateMethod()) //构建Java类 .build();}//生成bindView方法的代码private MethodSpec generateMethod() { //获取所有注解的类的类名 ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName().toString()); //构建方法--方法名 return MethodSpec.methodBuilder("bindView") //public方法 .addModifiers(Modifier.PUBLIC) //返回void .returns(void.class) //方法传参(参数全类名,参数名) .addParameter(className, ParamName) //方法代码 .addCode(generateMethodCode()) .build();}//生成bindView方法里面代码的代码private String generateMethodCode() { StringBuilder code = new StringBuilder(); for (int id : mVariableElements.keySet()) { VariableElement variableElement = mVariableElements.get(id); //使用注解的属性的名称 String name = variableElement.getSimpleName().toString(); //使用注解的属性的类型 String type = variableElement.asType().toString(); //view.name = (type)view.findViewById(id) String findViewCode = ParamName + "." + name + "=(" + type + ")" + ParamName + ".findViewById(" + id + ");\n"; code.append(findViewCode); } return code.toString();}
上面定义了三个方法,一个生成类,另外两个是对bindView方法的代码生成,相信大家细心点看还是看得懂的。
所有的代码工作做好了后,紧接着需要去注册和启动BinderProcessor
此处是google提供的AutoService注解,用来扫描工程注解的扫描器,第二个要注意的地方是启动扫描器:
在main下面生成javax.annotation.processing.Processor文件,里面写上要被启动的扫描器:
com.single.router_compiler.BinderProcessor
使用由于我们生成的APT代码,肯定只有在运行期才能使用的,所以在编译之前肯定是找不到***_ViewBinding类的,因此咋们得需要写个反射调用该类的工具类:
public class BinderViewTools { public static void init(Activity activity) { Class clazz = activity.getClass(); try { Class<?> bindClass = Class.forName(clazz.getName() + "_ViewBinding"); Method bind = bindClass.getMethod("bindView", class); bind.invoke(bindClass.newInstance(), activity); } catch (Exception e) { e.printStackTrace(); } }}
不熟悉反射的伙伴们可以看下反射如何使用,这里就不多说了。更多反射知识,在activity中直接使用:
BinderViewTools.init(this);
接着编译下工程,在app目录的build下面可以看到生成了activity对应的*** _ViewBinding类:
完整代码https://github.com/xiangcman/Android_Demos
推荐阅读Retrofit 结合 Lifecycle, 将 Http 生命周期管理到极致Glide 架构设计艺术
