Android编译期插桩,让程序自己写代码(一)
前言
近些年,编译期插桩技术在Android圈越来越普遍。无论是可以生成JAVA源码的ButterKnief、Dagger,还是操作字节码的VirtualAPK,甚至是新兴的语言Kotlin都用到了编译期插桩技术。学习这门技术对我们了解这些框架的原理十分有帮助。另外,我们通过这种技术可以抽离出复杂、重复的代码,降低程序耦合性,提高代码的可复用性,提高开发效率。因而,理解编译期插桩技术十分必要。在详情这项技术之前,我们先来理解一下Android代码的编译过程以及插桩位置。话不多说,直接上图。
image.png
APT
APT(Annotation Processing Tool)是一种编译期注解解决器。它通过定义注解和解决器来实现编译期生成代码的功能,并且将生成的代码和源代码一起编译成.class文件。
代表框架:ButterKnife、Dagger、ARouter、EventBus3、DataBinding、AndroidAnnotation等。
在详情如何应用APT技术之前,我们先来理解少量相关的知识。
一、Element
1.简介
Element是一种在编译期形容.java文件静态结构的一种类型,它可能表示一个package、一个class、一个method或者者一个field。Element的比较应该使用equals,由于编译期间同一个Element可能会用两个对象表示。JDK提供了以下5种Element。
image.png
2.Element的存储结构
编译器采用相似Html的Dom树来存储Element。我们用下面的Test.java来具体说明。
//PackageElementpackage me.zhangkuo.compile;//TypeElementpublic class Test { //VariableElement private String name; //ExecutableElement private Test(){ } //ExecutableElement public void setName(/* TypeParameterElement */ String name) { this.name = name; }}Test.java用Element树结构形容如下:
image.png
我们可以看到 setName(String name)的ExecutableElement中并没有子节点TypeParameterElement。这是由于TypeParameterElement没有被归入到Element树中。不过我们可以通过ExecutableElement的getTypeParameters()方法来获取。
此外,再给大家详情两个Element中十分有用的方法。
public interface Element extends AnnotatedConstruct { //获取父Element Element getEnclosingElement(); //获取子Element的集合 List<? extends Element> getEnclosedElements();}二、TypeMirror
Element有一个asType()方法用来返回TypeMirror。TypeMirror表示 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应该是编译期插桩最简单的一种技术,通过三步即可以完成。
- 定义编译期注解。
我们新添加一个Java Library Module命名为apt_api,编写注解类BindView。
@Retention(RetentionPolicy.Class)@Target(ElementType.FIELD)public @interface BindView {}这里简单详情一下RetentionPolicy。RetentionPolicy是一个枚举,它的值有三种:SOURCE、CLASS、RUNTIME。
- SOURCE:不参加编译,让开发者使用。
- CLASS:参加编译,运行时不可见。给编译器使用。
- RUNTIME:参加编译,运行时可见。给编译器和JVM使用。
- 定义注解解决器。
同样,我们需要新添加一个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<String> 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提供的四个方法:getSupportedAnnotationTypes、getSupportedSourceVersion、init、process。
getSupportedAnnotationTypes表示解决器可以解决哪些注解。这里返回的是我们之前定义的BindView。除了重写方法之外,还可用通过注解来实现。@SupportedAnnotationTypes(value = {"me.zhangkuo.apt.annotation.BindView"})getSupportedSourceVersion表示解决器可以解决的Java版本。这里我们采用最新的JDK版本即可以了。同样,我们也可以通过注解来实现。@SupportedSourceVersion(value = SourceVersion.latestSupported())init方法主要用来做少量准备工作。我们一般在这里初始化几个工具类。上述代码我们初始了与元素相关的工具类elementUtils、与日志相关的工具类messager、与文件相关的filer以及与类型相关工具类typeUtils。我们接下来会看到process主要就是通过这几个类来生成代码的。process用来完成具体的程序写代码功能。在具体详情process之前,请允许我先推荐一个库:javapoet。javapoet是由神奇的square公司开源的,它提供了非常人性化的api,来帮助开发者生成.java源文件。它的README.md文件为我们提供了丰富的例子,是我们学习的主要工具。private Map<TypeElement, List<Element>> 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<Element> 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<Element>>> entries = elementPackage.entrySet(); Iterator<Map.Entry<TypeElement,List<Element>>> iterator = entries.iterator(); while (iterator.hasNext()){ Map.Entry<TypeElement,List<Element>> entry = iterator.next(); //类元素 TypeElement parent = entry.getKey(); //当前类元素下,注解了BindView的元素 List<Element> 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<Element> 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 temp = ($T)target",parent,parent); for (Element element : elementList) { int id = element.getAnnotation(BindView.class).value(); bindViewMethod.addStatement("temp.$N = temp.findViewById($L)", element.getSimpleName().toString(), id); } return bindViewMethod.build(); }process的代码比较长,但是它的逻辑非常简单看,主要分为收集数据和生成代码两部分。我为关键的地方都加了注释,就不再详细解释了。到这里我们基本上完成了注解器的编写工作。- 使用注解
在build.gradle中引入我们定义的注解和注解解决器。
implementation project(':apt_api') annotationProcessor project(":apt_processor")应用注解
public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_content) TextView tvContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.inject(this); tvContent.setText("这就是ButterKnife的原理"); }}到这里,这篇文件就结束了。什么?你还没说
ButterKnife这个类呢。好吧,这个真的很简单,直接贴代码吧。public class ButterKnife { static final Map<Class<?>, Constructor<? extends ViewBinding>> BINDINGS = new LinkedHashMap<>(); public static void inject(Object object) { if (object == null) { return; } try { Class<?> cls = object.getClass(); Constructor<? extends ViewBinding> constructor = findBindingConstructorForClass(cls); ViewBinding viewBinding = constructor.newInstance(); viewBinding.bindView(object); } catch (Exception e) { } } private static Constructor<? extends ViewBinding> findBindingConstructorForClass(Class<?> cls) throws Exception { Constructor<? extends ViewBinding> constructor = BINDINGS.get(cls); if (constructor == null) { String className = cls.getName(); Class<?> bindingClass = cls.getClassLoader().loadClass(className + "_ViewBinding"); constructor = (Constructor<? extends ViewBinding>) bindingClass.getConstructor(); BINDINGS.put(cls, constructor); } return constructor; }}
【附】相关架构及资料
加群 Android IOC架构设计领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起探讨交流处理问题。
image
领取方式:
点赞+加群免费获取 Android IOC架构设计
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android编译期插桩,让程序自己写代码(一)