一篇文章教你如何在Android编译期插桩,让程序学会自己写代码(1)

this.name = name;

}

}

Test.java用Element树结构描述如下:

我们可以看到 setName(String name)ExecutableElement中并没有子节点TypeParameterElement。这是因为TypeParameterElement没有被纳入到Element树中。不过我们可以通过ExecutableElementgetTypeParameters()方法来获取。

此外,再给大家介绍两个Element中十分有用的方法。

public interface Element extends AnnotatedConstruct {

//获取父Element

Element getEnclosingElement();

//获取子Element的集合

List<? extends Element> getEnclosedElements();

}

二、TypeMirror

============

Element有一个asType()方法用来返回TypeMirrorTypeMirror表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。我们一般用TypeMirror进行类型判断。如下段代码,用来比较元素所描述的类型是否是Activity的子类。

/**

  • 类型相关工具类

*/

private Types typeUtils;

/**

  • 元素相关的工具类

*/

private Elements elementUtils;

private static final String ACTIVITY_TYPE = “android.app.Activity”;

private boolean isSubActivity(Element element){

//获取当前元素的TypeMirror

TypeMirror elementTypeMirror = element.asType();

//通过工具类Elements获取Activity的Element,并转换为TypeMirror

TypeMirror viewTypeMirror = elementUtils.getTypeElement(ACTIVITY_TYPE).asType();

//用工具类typeUtils判断两者间的关系

return typeUtils.isSubtype(elementTypeMirror,viewTypeMirror)

}

三、一个简单的ButterKnife

==================

这一节我们通过编写一个简单的ButterKnife来介绍一下如何编写一个APT框架。APT应该是编译期插桩最简单的一种技术,通过三步就可以完成。

1、定义编译期注解。


我们新增一个Java Library Module命名为apt_api,编写注解类BindView。

@Retention(RetentionPolicy.Class)

@Target(ElementType.FIELD)

public @interface BindView {

}

这里简单介绍一下RetentionPolicyRetentionPolicy是一个枚举,它的值有三种:SOURCE、CLASS、RUNTIME。

  • SOURCE:不参与编译,让开发者使用。

  • CLASS:参与编译,运行时不可见。给编译器使用。

  • RUNTIME:参与编译,运行时可见。给编译器和JVM使用。

**2、**定义注解处理器。


同样,我们需要新增一个Java Library Module命名为apt_processor

我们需要引入两个必要的依赖:一个是我们新增的module apt_annotation,另一个是google的com.google.auto.service:auto-service:1.0-rc3(以下简称auto-service)。

implementation project(‘:apt_api’)

api ‘com.google.auto.service:auto-service:1.0-rc3’

新增一个类 ButterKnifeProcessor,继承 AbstractProcessor

@AutoService(Processor.class)

public class ButterKnifeProcessor extends AbstractProcessor {

/**

  • 元素相关的工具类

*/

private Elements elementUtils;

/**

  • 文件相关的工具类

*/

private Filer filer;

/**

  • 日志相关的工具类

*/

private Messager messager;

/**

  • 类型相关工具类

*/

private Types typeUtils;

@Override

public Set getSupportedAnnotationTypes() {

return Collections.singleton(BindView.class.getCanonicalName());

}

@Override

public SourceVersion getSupportedSourceVersion() {

return SourceVersion.RELEASE_7;

}

@Override

public synchronized void init(ProcessingEnvironment processingEnvironment) {

super.init(processingEnvironment);

elementUtils = processingEnv.getElementUtils();

filer = processingEnv.getFiler();

messager = processingEnv.getMessager();

typeUtils = processingEnv.getTypeUtils();

}

@Override

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

return false;

}

}

auto-service为我们简化了定义注解处理器的流程。@AutoService是就是由auto-service提供的,其作用是用来告诉编译器我们定义的ButterKnifeProcessor是一个编译期注解处理器。这样在编译时ButterKnifeProcessor才会被调用。

我们还重写了AbstractProcessor提供的四个方法:getSupportedAnnotationTypesgetSupportedSourceVersioninitprocess

  • getSupportedAnnotationTypes表示处理器可以处理哪些注解。这里返回的是我们之前定义的BindView。除了重写方法之外,还可用通过注解来实现。

@SupportedAnnotationTypes(value = {“me.zhangkuo.apt.annotation.BindView”})

  • getSupportedSourceVersion表示处理器可以处理的Java版本。这里我们采用最新的JDK版本就可以了。同样,我们也可以通过注解来实现。

@SupportedSourceVersion(value = SourceVersion.latestSupported())

  • init方法主要用来做一些准备工作。我们一般在这里初始化几个工具类。上述代码我们初始了与元素相关的工具类elementUtils、与日志相关的工具类messager、与文件相关的filer以及与类型相关工具类typeUtils。我们接下来会看到process主要就是通过这几个类来生成代码的。

  • process用来完成具体的程序写代码功能。在具体介绍process之前,请允许我先推荐一个库:javapoetjavapoet是由神奇的square公司开源的,它提供了非常人性化的api,来帮助开发者生成.java源文件。它的README.md文件为我们提供了丰富的例子,是我们学习的主要工具。

private Map<TypeElement, List> elementPackage = new HashMap<>();

private static final String VIEW_TYPE = “android.view.View”;

private static final String VIEW_BINDER = “me.zhangkuo.apt.ViewBinding”;

@Override

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

if (set == null || set.isEmpty()) {

return false;

}

elementPackage.clear();

Set<? extends Element> bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class);

//收集数据放入elementPackage中

collectData(bindViewElement);

//根据elementPackage中的数据生成.java代码

generateCode();

return true;

}

private void collectData(Set<? extends Element> elements){

Iterator<? extends Element> iterable = elements.iterator();

while (iterable.hasNext()) {

Element element = iterable.next();

TypeMirror elementTypeMirror = element.asType();

//判断元素的类型是否是View或者是View的子类型。

TypeMirror viewTypeMirror = elementUtils.getTypeElement(VIEW_TYPE).asType();

if (typeUtils.isSubtype(elementTypeMirror, viewTypeMirror) || typeUtils.isSameType(elementTypeMirror, viewTypeMirror)) {

//找到父元素,这里认为是@BindView标记字段所在的类。

TypeElement parent = (TypeElement) element.getEnclosingElement();

//根据parent不同存储的List中

List parentElements = elementPackage.get(parent);

if (parentElements == null) {

parentElements = new ArrayList<>();

elementPackage.put(parent, parentElements);

}

parentElements.add(element);

}else{

throw new RuntimeException(“错误处理,BindView应该标注在类型是View的字段上”);

}

}

}

private void generateCode(){

Set<Map.Entry<TypeElement,List>> entries = elementPackage.entrySet();

Iterator<Map.Entry<TypeElement,List>> iterator = entries.iterator();

while (iterator.hasNext()){

Map.Entry<TypeElement,List> entry = iterator.next();

//类元素

TypeElement parent = entry.getKey();

//当前类元素下,注解了BindView的元素

List elements = entry.getValue();

//通过JavaPoet生成bindView的MethodSpec

MethodSpec methodSpec = generateBindViewMethod(parent,elements);

String packageName = getPackage(parent).getQualifiedName().toString();

ClassName viewBinderInterface = ClassName.get(elementUtils.getTypeElement(VIEW_BINDER));

String className = parent.getQualifiedName().toString().substring(

packageName.length() + 1).replace(‘.’, ‘$’);

ClassName bindingClassName = ClassName.get(packageName, className + “_ViewBinding”);

try {

//生成 className_ViewBinding.java文件

JavaFile.builder(packageName, TypeSpec.classBuilder(bindingClassName)

.addModifiers(PUBLIC)

.addSuperinterface(viewBinderInterface)

.addMethod(methodSpec)

.build()

).build().writeTo(filer);

} catch (IOException e) {

e.printStackTrace();

}

}

}

private MethodSpec generateBindViewMethod(TypeElement parent,List elementList) {

ParameterSpec.Builder parameter = ParameterSpec.builder(TypeName.OBJECT, “target”);

MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder(“bindView”);

bindViewMethod.addParameter(parameter.build());

bindViewMethod.addModifiers(Modifier.PUBLIC);

bindViewMethod.addStatement(“ T t e m p = ( T temp = ( Ttemp=(T)target”,parent,parent);

for (Element element :

elementList) {

int id = element.getAnnotation(BindView.class).value();

bindViewMethod.addStatement(“temp. N = t e m p . f i n d V i e w B y I d ( N = temp.findViewById( N=temp.findViewById(L)”, element.getSimpleName().toString(), id);

}

return bindViewMethod.build();

}

process的代码比较长,但是它的逻辑非常简单看,主要分为收集数据和生成代码两部分。我为关键的地方都加了注释,就不再详细解释了。到这里我们基本上完成了注解器的编写工作。

3、使用注解


在build.gradle中引入我们定义的注解和注解处理器。

最后,面试前该准备哪些资源复习?

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一下我面试期间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《Android开发七大模块核心知识笔记》

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

《960全网最全Android开发笔记》

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
2)]

[外链图片转存中…(img-faEB2uTv-1715212448685)]

《960全网最全Android开发笔记》

[外链图片转存中…(img-ewxIzKjW-1715212448686)]

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值