关键词:
一 前言
android中现今流行的各大框架,比如ButterFly、eventBus、OrmLite、Retrofit等都使用注解,注解是什 么呢?注解就是元数据,可以理解为属性、方法、类等的一个说明,具体详解可百度,也可移步我的另一篇注解原理详解。一下就以ButterFly为例,解读徒手打造一个FinderView的框架。
获取注解的元数据的方式有以下两种:
1、直接通过Class|Method|Field.getAnnotation(xxxAnnotation.class)获取注解实例,在获取元数据, 具体可查看注解原理详解。
2、通过注解处理器APT来获取。
这里使用的是方式二。注解器处理是在build编译时执行的
二 原理
1、定义Method、Field的注解类分别为OnClick、BindView,分别应用于Method和Field
2、apt注解处理器解析注解类,获取Method/Field,随后通过javapoet框架创建一个类为xxxFind实现为每 个Field生成findViewById()的实现方法,为每个Method生成OnClickListener监听器并实现调用被注解 的Method
3、通过工具类Inject(activity)调用,实现反射xxxFind,调用上述的方法实现Method、Field初始化
三 详解
注:
为了减少麻烦 这里需要使用如下依赖:
项目的build.gradle中
向仓库中添加组件对apt的依赖 :classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
module中的build.gradle引用两个库:
apt编译执行时的库 : compile 'com.squareup:javapoet:1.7.0'
生成java代码的库: compile 'com.google.auto.service:auto-service:1.0-rc2'
1.明确我们的注解是在编译时期进行的,而且只用在控件属性和点击方法,所以定义了如下注解类:
(1)方法注解类OnClick:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick
int[] value();
(2)控件属性注解类BindView:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView
int value();
因为初始化Method和Field都需要控件id,所以每个注解都要带控件id,所以需要value值,而且是必须的。
2.apt的解析和生成自动初始化类以及相关方法实现
如要生成如下格式:
public class MainActivity$$Finder implements Finder<MainActivity>
@Override
public void inject(final MainActivity host, Object source, Provider provider)
host.mTextView = (TextView)(provider.findView(source, 2131427414));
host.mButton = (Button)(provider.findView(source, 2131427413));
host.mEditText = (EditText)(provider.findView(source, 2131427412));
View.OnClickListener listener;
listener = new View.OnClickListener()
@Override
public void onClick(View view)
host.onButtonClick();
;
provider.findView(source, 2131427413).setOnClickListener(listener);
listener = new View.OnClickListener()
@Override
public void onClick(View view)
host.onTextClick();
;
provider.findView(source, 2131427414).setOnClickListener(listener);
apt代码走起:
apt需要继承AbstractProcessor并实现如下方法:
/**
* 使用 Google 的 auto-service 库可以自动生成 META-INF/services/javax.annotation.processing.Processor 文件
*/
@AutoService(Processor.class)//用 @AutoService 来注解这个处理器,可以自动生成配置信息
public class ViewFinderProcesser extends AbstractProcessor
private Filer mFiler; //文件的类
private Elements mElementUtils; //元素相光类
private Messager mMessager;//日志相关类,也可以是用java的system.out输出日志
/***初始化会调用,一个处理器只执行一次*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
super.init(processingEnv);
/**
* 支持的注解类型
* @return types 指定哪些注解应该被注解处理器注册
*/
@Override
public Set<String> getSupportedAnnotationTypes()
/**
* @return 指定使用的 Java 版本。通常返回 SourceVersion.latestSupported()。
*/
@Override
public SourceVersion getSupportedSourceVersion()
return SourceVersion.latestSupported();
private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
// return true;//返回是True,那么后续的处理器就不会在处理
return false;//返回是false,那么后续处理器会继续处理它们。一个处理器可能总是返回同样的逻辑值,或者是根据选项改变结果。
最主要的方法:
1.init():初始化方法,一个处理器执行一次
2.getSupportedAnnotationTypes():指定这个处理器只处理哪些注解类
3.getSupportedSourceVersion():指定处理器使用的jdk版本
4.process(annotations,RoundEnvironment ):处理器最主要的方法,用于处理注解
annotations:是此次处理注解类的集合
RoundEnvironment :当作处理器和元素之间的上下文,就是个通信桥梁
接下来主要看看主要分析:
1.引入自动处理器的库之后在处理器实现类中使用注解的方式编译器就会执行这个类
@AutoService(Processor.class)//用 @AutoService 来注解这个处理器,可以自动生成配置信息
public class ViewFinderProcesser extends AbstractProcessor
2.init()初始化文件相关类、元素(包括类、属性、方法等)相关类、日志管理类
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
super.init(processingEnv);
System.out.println("=== init ");
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
3.这里标注处理器要处理的注解类:有BindView和OnClick
/**
* 支持的注解类型
* @return types 指定哪些注解应该被注解处理器注册
*/
@Override
public Set<String> getSupportedAnnotationTypes()
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
4.通过上下文roundEnv处理注解器
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
mAnnotatedClassMap.clear();
try
info("process== %s", annotations.toString());//
processBindView(roundEnv);
processOnClick(roundEnv);
catch (IllegalArgumentException e)
info("Generate file failed,1111 reason: %s", e.getMessage());
return false; // stop process
// System.out.println("=== annotatedClass "+mAnnotatedClassMap.size());
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values())
try
// System.out.println("=== annotatedClass "+annotatedClass.getFullClassName());
info("Generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateFinder().writeTo(mFiler);
catch (IOException e)
info("Generate file failed,2222 reason: %s", e.getMessage());
return false;
// return true;//不再执行这个
return false;//
mAnnotatedClassMap:是一个map用于缓存每一个注解相关信息
processBindView(roundEnv);
processOnClick(roundEnv);
这两个方法实现都差不多,如下:
private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException
//get BindView AnnotionType for all Current Class of Elements
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class))
// TODO: 16/8/4
AnnotatedClass annotatedClass = getAnnotatedClass(element);
info("element name %s", element.getSimpleName());
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
private void processOnClick(RoundEnvironment roundEnv)
for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class))
AnnotatedClass annotatedClass = getAnnotatedClass(element);
info("element OnClick %s", element.getSimpleName());
OnClickMethod method = new OnClickMethod(element);
annotatedClass.addMethod(method);
主要看这个方法:
roundEnv.getElementsAnnotatedWith(OnClick.class)
roundEnv.getElementsAnnotatedWith(BindView.class)
这里是获取被注解类OnClick和BindView注解的所有元素(包括类、方法、属性等)
随后调用getAnnotatedClass(element);
private AnnotatedClass getAnnotatedClass(Element element)
TypeElement classElement = (TypeElement) element.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
info("element fullClassName %s", fullClassName+" e "+element.getSimpleName());
AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
if (annotatedClass == null)
annotatedClass = new AnnotatedClass(classElement, mElementUtils);
mAnnotatedClassMap.put(fullClassName, annotatedClass);
return annotatedClass;
主要是获取当前类名,创建一个AnnotationClass实例,将类名作为map的key,实例作为value缓存
为了帮助理解补充如下:
element.getEnclosingElement();// 获取父元素
element.getEnclosedElements();// 获取子元素
其中父元素、子元素是根据xml中dom树来决定的不是java中继承关系上的父子元素。
所以这里使用TypeElement classElement = (TypeElement) element.getEnclosingElement();
获取父元素并强转成TypeElement。那怎么知道是TypeElement而不是其VariableElement。这就要理解关系如下
public class Foo // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo() // ExecuteableElement
public void setA( // ExecuteableElement
int newA // TypeElement
)
这里表示每个类型关系:映射到dom树的层级关系中。如定义的是方法、属性那么element实际类型是ExecuteableElement和VariableElement,那么element.getEnclosingElement();就是获取父元素,父元素就是TypeElement.如果了解html查找摸个元素层级,应该会很好理解。
以上补充完毕,代码相信读者都很好理解了,言归正传回到之前的思路:
调用getAnnatationClasss()之后,将当前类的所有的注解类转化成一个AnnotationClass缓存到Map中
也就是Map中缓存了一个以当前类名为key,AnnotationClass为Value的Map,当前类有使用BindView或OnClick注解。在这里Map会缓存两个AnnotationClass。如下:
Map的key:
com.sample.MainActivity : 在MainActivity下使用了注解,所以会生成一个对应的AnnotationClass
com.sample.SecondActivity:在SecondActivity下使用了注解,所以会生成一个对应的AnnotationClass
获取到AnnotationClass实例之后,回到方法出:再次贴出方法:
private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException
//get BindView AnnotionType for all Current Class of Elements
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class))
// TODO: 16/8/4
AnnotatedClass annotatedClass = getAnnotatedClass(element);
info("element name %s", element.getSimpleName());
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
new BindViewField(element)的实现如下:
public BindViewField(Element element) throws IllegalArgumentException
if (element.getKind() != ElementKind.FIELD)
throw new IllegalArgumentException(
String.format("Only fields can be annotated with @%s", BindView.class.getSimpleName()));
mFieldElement = (VariableElement) element;
BindView bindView = mFieldElement.getAnnotation(BindView.class);
mResId = bindView.value();
if (mResId < 0)
throw new IllegalArgumentException(
String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
mFieldElement.getSimpleName()));
主要是解析注解的元数据,获取控件属性的id,并保存到实例当中,
new OnClickMethod(element)和BindViewField(element)一模一样,不重复解析。
创建相应的类:方法实例和属性实例 随后通过addxxx()加入到annotation实例中,最后看看map调用处
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values())
try
// System.out.println("=== annotatedClass "+annotatedClass.getFullClassName());
info("Generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateFinder().writeTo(mFiler);
catch (IOException e)
info("Generate file failed,2222 reason: %s", e.getMessage());
return false;
最主要的代码就是annotatedClass.generateFinder().writeTo(mFiler);也就是生成初始化代码类以及实现,然后写入到文件中,也就是生成了一个java类,类名是当前类名$$Finder
AnnotationClass如下:
public class AnnotatedClass
public TypeElement mClassElement;//父元素:这里表示当前类
public List<BindViewField> mFields;//被注解的元素:这里是属性
public List<OnClickMethod> mMethods;//被注解的元素:这里是方法
public Elements mElementUtils;//元素操作类
public AnnotatedClass(TypeElement classElement, Elements elementUtils)
this.mClassElement = classElement;
this.mFields = new ArrayList<>();
this.mMethods = new ArrayList<>();
this.mElementUtils = elementUtils;
public String getFullClassName()
return mClassElement.getQualifiedName().toString();
public void addField(BindViewField field)
mFields.add(field);
public void addMethod(OnClickMethod method)
mMethods.add(method);
/***
*生成实现类,实现开头3.2贴出的实现格式
*/
public JavaFile generateFinder()
// method inject(final T host, Object source, Provider provider)
MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.PROVIDER, "provider");
for (BindViewField field : mFields)
// find views
injectMethodBuilder.addStatement("host.$N = ($T)(provider.findView(source, $L))", field.getFieldName(),
ClassName.get(field.getFieldType()), field.getResId());
if (mMethods.size() > 0)
injectMethodBuilder.addStatement("$T listener", TypeUtil.ANDROID_ON_CLICK_LISTENER);
for (OnClickMethod method : mMethods)
// declare OnClickListener anonymous class
TypeSpec listener = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeUtil.ANDROID_VIEW, "view")
.addStatement("host.$N()", method.getMethodName())
.build())
.build();
injectMethodBuilder.addStatement("listener = $L ", listener);
for (int id : method.ids)
// set listeners
injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
// generate whole class
TypeSpec finderClass = TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType())))
.addMethod(injectMethodBuilder.build())
.build();
String packageName = mElementUtils.getPackageOf(mClassElement).getQualifiedName().toString();
return JavaFile.builder(packageName, finderClass).build();
这里主要用到了javapoet库,所以要讲解主要用到的方法是什么玩意:
1.addModifiers:添加修饰符:如public private protected等
2.addAnnotation:添加注解
3.addParameter:添加参数 如Override
4.addStatement("$L listerner",ClassName):添加语句:
如 :
injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
$L是变量,id是这个变量的值
5.returns(TypeName.VOID):返回类型
6.addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)实现接口
7.TypeSpec.anonymousClassBuilder("")构建一个匿名内部类
8.MethodSpec.methodBuilder("inject")构建一个方法名为inject
9.TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")构建一个实体类,名为当前类名 $$Finder
10. JavaFile.builder(packageName, finderClass).build();构造这模式创建JavaFile实例
11.ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType()))
泛型参数。第一个参数是实现的接口,第二个参数是具体的泛型类型
最后调用了javaFile.writeTo(mFiler);将构建的内容写入文件,即生成了一个java文件。编译器也会对齐进行编译。
这样自动生成代码就完成了。接下来看看如何实现调用的?
自动生成方法:inject(final T host, Object source, Provider provider)
大致思路:
为了解耦Provider是个接口具体实现初始化的地方就是Provider的方法findView();
所以这里有两个实现:
ActivityProvider:在activity调用ViewInject.inject(activity),使用这个findView()
public class ActivityProvider implements Provider
@Override
public Context getContext(Object source)
return ((Activity) source);
@Override
public View findView(Object source, int id)
return ((Activity) source).findViewById(id);
ViewProvide:
public class ViewProvider implements Provider
@Override
public Context getContext(Object source)
return ((View) source).getContext();
@Override
public View findView(Object source, int id)
return ((View) source).findViewById(id);
一般使用:
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewFinder.inject(this);
ViewFinder:
public class ViewFinder
private static final ActivityProvider PROVIDER_ACTIVITY = new ActivityProvider();
private static final ViewProvider PROVIDER_VIEW = new ViewProvider();
private static final Map<String, Finder> FINDER_MAP = new HashMap<>();
public static void inject(Activity activity)
inject(activity, activity, PROVIDER_ACTIVITY);
public static void inject(View view)
inject(view, view);
public static void inject(Object host, View view)
// for fragment
inject(host, view, PROVIDER_VIEW);
/****
*
* @param host 是传入的宿主,属性方法所在的对象 实现解耦的地方
* @param source 传入的source.findViewById()的source:可能是View/Activity/Fragement
* @param provider 具体实现findViewByid()复制给属性的具体实现 ViewProvider/ActivityProvide/FragementProvider
*/
public static void inject(Object host, Object source, Provider provider)
String className = host.getClass().getName();
try
Finder finder = FINDER_MAP.get(className);
if (finder == null)
Class<?> finderClass = Class.forName(className + "$$Finder");
finder = (Finder) finderClass.newInstance();
FINDER_MAP.put(className, finder);
finder.inject(host, source, provider);
catch (Exception e)
throw new RuntimeException("Unable to inject for " + className, e);
具体的参数host、source、provider的职责在参数上写的很清楚了。
传入的宿主,然后生成一个宿主类创建,通过反射实例化通过javapoet自动生成的具体实现,随后调用它的具体实现方法进行初始化。
看看ViewFinder.inject()有如下3个重载方法,第四个是留作扩展
1.inject(activity):用于在activity初始化控件
2.inject(View):用于view初始化子view:比如手动载入一个xml
3.inject(Fragment):用于fragment方法
4.inject(Hold,source): Hold作为泛型的具体实现,自己生成hold的自动代码生成,仿造xxx$$Finder的实现。
这样就完成了。
总结:
1.ViewFinde.inject(this);会调用相应的方法,
2.通过反射调用javapoet生成的代码实现初始化
借此来了解整个apt工作方式。之后还会讲解AOP之Aspectj和javassist框架的实际应用。
此文章的解读原作者博客:如有侵权请告知
https://brucezz.itscoder.com/use-apt-in-android
demo:https://download.csdn.net/download/zhongwn/10426802
asm(代码片段)
...?分别解释下这几个名词APT:APT(AnnotationProcessingTool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作... 查看详情
04注解处理器(apt)是什么?——《android打怪升级之旅》(代码片段)
...解,这篇咱们科普一下注解的其中一种用途——注解处理器(APT),文章会手把手的帮助大家学会APT的使用。一、定义注解处理器(AnnotationProcessingTool,简称APT),是JDK提供的工具,用于在编译... 查看详情
注解处理器apt在java中的实现(代码片段)
...文全名(AnnotationProcessorTool),即:注解处理器。它是 javac 的一个工具,这是Sun为了帮助注解的处理过程而提供的工具,apt被设计为操作Java源文件,而不是编译后的类。作用阶段示意图如下:具... 查看详情
框架手写系列---apt注解处理器方式实现butterknife框架(代码片段)
一、ButterKnifeButterKnife作为常用框架之一,主要用于简化代码,减少重复代码。这里主要注重原理与核心,将分步骤手写它的核心代码。ButterKnife最常用的是去除代码中的findViewById,以注解的方式代替原有的代码... 查看详情
注解处理器(apt)是什么?(代码片段)
...解,这篇咱们科普一下注解的其中一种用途——注解处理器(APT),文章会手把手的帮助大家学会APT的使用,并使用简单的例子来进行练习。一、定义注解处理器(AnnotationProcessingTool,简称APT)ÿ... 查看详情
spring源码窥探之:注解方式的aop原理
AOP入口代码分析通过注解的方式来实现AOP1.@EnableAspectJAutoProxy通过@Import注解向容器中注入了AspectJAutoProxyRegistrar这个类,而它在容器中的名字是org.springframework.aop.config.internalAutoProxyCreator。2.AspectJAutoProxyRegistrar实现了ImportBean 查看详情
android编译时注解处理apt(代码片段)
一、注解在使用Java语言开发的过程中,我们会经常看到各种各样的注解,@Override(表示方法的重写),@Deprecated(标记过时的元素方法,类或属性),@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@Drawa... 查看详情
android编译时注解处理apt(代码片段)
一、注解在使用Java语言开发的过程中,我们会经常看到各种各样的注解,@Override(表示方法的重写),@Deprecated(标记过时的元素方法,类或属性),@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@Drawa... 查看详情
spring源码学习之aop源码分析
...注解就是Aop的入口了。这个注解的作用就是在Spring的后置处理器中添加一个处理器来处理springBean,使之成为一个代理对象。1@Target({ElementType.TYPE})2@Retention(RetentionPolicy.RUNTIME)3@Documented4@Import({Aspec 查看详情
spring的第四天aop之注解版
Spring的第四天AOP之注解版ssm框架 spring 在上一篇博客中,介绍了Spring的AOP的xml版本的使用,在这篇博客中,我将介绍一下,注解版的使用。常用注解注解通知@After通知方法会在目标方法返回或抛出异常后调用@AfterRetruening... 查看详情
注解深入浅出(二apt)
...ool,它是javac的一个工具,中文意思为编译时注解处理器。APT可以用来在编译时扫描和处理注解。通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省... 查看详情
android编译时注解处理apt(代码片段)
一、注解在使用Java语言开发的过程中,我们会经常看到各种各样的注解,@Override(表示方法的重写),@Deprecated(标记过时的元素方法,类或属性),@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@Drawa... 查看详情
android编译时注解处理apt(代码片段)
一、注解在使用Java语言开发的过程中,我们会经常看到各种各样的注解,@Override(表示方法的重写),@Deprecated(标记过时的元素方法,类或属性),@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@Drawa... 查看详情
一起学spring之注解和schema方式实现aop
...们了解了通过实现接口和XML配置的方式来实现AOP,在实现注解方式AOP之前,先了解一下AspectJ。AspectJ是一个面向切面的框架,它扩展了Java语言,定义了AOP语法,能够在编译时实现代码的注入。Spring通过集成ApsectJ实现了以注解方... 查看详情
字节码插桩android打包流程|android中的字节码操作方式|aop面向切面编程|apt编译时技术
文章目录一、Android中的Java源码打包流程1、Java源码打包流程2、字符串常量池二、Android中的字节码操作方式一、Android中的Java源码打包流程Java程序在Java虚拟机执行前,需要先将Java源码通过javac编译成.class字节码文件,然后才能在虚... 查看详情
arouter之注解处理器
...都依赖路由表,而路由表的生成是依赖注解及编译时注解处理器,在编译期间,路由注解器会解析路由注解信息,并按照规则、模板自动生成路由、路由表等java文件。注解处理器是在编译期解析源码中的注解信息,在根据规则... 查看详情
android-asm字节码插桩与apt原理补充
...#xff0c;自动加载文件里所在的类。JavaC源码分析SPI机制注解处理器的很多东西都是依靠Round这个辅助工具返回值的作用?注解是否往下传递,如果是true,就不往 查看详情
aop之aspectj在android中的解读(代码片段)
一前言 在没有接触AOP切面编程时,总觉得它是一门特神奇的,特遥不可及的技术,直到公司做无埋,用hook所有监听器的直男方式,遇到无底洞的大坑之后,才痛定思痛执着了解AOP切面编程。 对于AOP切面编程的意义,最主要是找到切... 查看详情