Android注解-编译时生成代码 (APT)
2016-08-09 22:52
423 查看
Android注解越来越引领潮流,比如 Dagger2, ButterKnife, EventBus3 等,他们都是注解类型,而且他们都有个共同点就是编译时生成代码,而不是运行时利用反射,这样大大优化了性能;而这些框架都用到了同一个工具就是:APT(Annotation Processing Tool ),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。
今天我们要自己实现的就是类似ButterKnife的简单的view初始化和点击事件;
先看下整个项目的目录结构:
![](https://img-blog.csdn.net/20160809150642141 )
inject :API module用来把生成的文件与控件相关联
viewinject-annotation :注解module
viewinject-compiler : 用来生成java文件module
先从最简单入手,注解moudle:
1.创建名字为
2.该module只有两个类:
1.
2.
注解module就完成了,下面看看API module
1.首先创建一个Android moudle 的
因为我们需要生成的文件是这么写的:
当然这个生成文件是根据自己需求生成,然后需要一个类来关联自己的activity类与生成的类:
使用方法就是:
host 表示注解 View 变量所在的类,也就是注解类
object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找
provider 是一个接口,定义了不同对象(比如 Activity、View 等)如何去查找目标 View,项目中分别为 Activity、View 实现了 Provider 接口(具体实现参考项目代码)
为了提高效率,避免每次注入的时候都去找
API module类就完成了
再看
首先创建名为
Javapoet是square一个工具,提供了各种 API 让你用各种姿势去生成 Java 代码文件,避免了徒手拼接字符串的尴尬。
首先创建
用
在 init() 可以初始化拿到一些实用的工具类。
这里涉及到了
![](https://img-blog.csdn.net/20160809155346505)
这个类的的基本内容就完成了,
现在创建
创建
然后重点就是生成Java代码文件的类:
具体的可以看javapoet的API,然后我们需要完善
实际使用
在项目的主module的build.gradle添加:
在自己的activity类使用:
点击makeProject 就编译完成后就可以在主项目module的/build/generated/source/apt/debug 目录下看到生成的java类文件了
一个学习级的apt项目就完成了。
项目源码
实战项目:Android6.0权限管理 工具,我用java重写别人的kotlin项目;地址:
https://github.com/a1018875550/PermissionDispatcher
今天我们要自己实现的就是类似ButterKnife的简单的view初始化和点击事件;
先看下整个项目的目录结构:
inject :API module用来把生成的文件与控件相关联
viewinject-annotation :注解module
viewinject-compiler : 用来生成java文件module
先从最简单入手,注解moudle:
1.创建名字为
viewinject-annotation的java类型module
2.该module只有两个类:
1.
BindView用来对成员变量进行注解,并且接收一个 int 类型的参数
* Created by JokAr on 16/8/6. */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int value(); }
2.
OnClick对方法进行注解,接收一个或一组 int 类型参数,相当于给一组 View 指定点击响应事件。
/** * Created by JokAr on 16/8/6. */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface OnClick { int[] value(); }
注解module就完成了,下面看看API module
1.首先创建一个Android moudle 的
inject,然后创建
interface
/** * Created by JokAr on 16/8/6. */ public interface Inject<T> { void inject(T host, Object object, Provider provider); }
/** * Created by JokAr on 16/8/6. */ public interface Provider { Context getContext(Object object); View findView(Object object, int id); }
因为我们需要生成的文件是这么写的:
public class MainActivity$$ViewInject implements Inject<MainActivity> { @Override public void inject(final MainActivity host, Object source, Provider provider) { host.textView = (TextView)(provider.findView(source, 2131427412)); host.button1 = (Button)(provider.findView(source, 2131427413)); View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View view) { host.click(); } } ; provider.findView(source, 2131427412).setOnClickListener(listener); } }
当然这个生成文件是根据自己需求生成,然后需要一个类来关联自己的activity类与生成的类:
/** * Created by JokAr on 16/8/6. */ public class ViewInject { private static final ActivityProvider activityProvider = new ActivityProvider(); private static final ViewProvider viewProvider = new ViewProvider(); private static final ArrayMap<String, Inject> injectMap = new ArrayMap<>(); public static void inject(Activity activity) { inject(activity, activity, activityProvider); } public static void inject(View view) { inject(view, view); } private static void inject(Object host, View view) { inject(host, view, viewProvider); } private static void inject(Object host, Object object, Provider provider) { String className = host.getClass().getName(); try { Inject inject = injectMap.get(className); if (inject == null) { Class<?> aClass = Class.forName(className + "$$ViewInject"); inject = (Inject) aClass.newInstance(); injectMap.put(className, inject); } inject.inject(host, object, provider); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
使用方法就是:
ViewInject.inject(this);
host 表示注解 View 变量所在的类,也就是注解类
object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找
provider 是一个接口,定义了不同对象(比如 Activity、View 等)如何去查找目标 View,项目中分别为 Activity、View 实现了 Provider 接口(具体实现参考项目代码)
为了提高效率,避免每次注入的时候都去找
Inject对象,用一个 Map 将第一次找到的对象缓存起来,后面用的时候直接从 Map 里面取。
API module类就完成了
再看
viewinject-compilermodule:
首先创建名为
iewinject-compiler的Java module ,然后在该module的buile.gradle加上一些依赖:
compile project(':viewinject-annotation') compile 'com.squareup:javapoet:1.7.0' compile 'com.google.auto.service:auto-service:1.0-rc2'
Javapoet是square一个工具,提供了各种 API 让你用各种姿势去生成 Java 代码文件,避免了徒手拼接字符串的尴尬。
auto-service主要用于注解 Processor,对其生成 META-INF 配置信息。
首先创建
ViewInjectProcesser类:
/** * Created by JokAr on 16/8/8. */ @AutoService(Processor.class) public class ViewInjectProcesser extends AbstractProcessor { private Filer mFiler; //文件相关的辅助类 private Elements mElementUtils; //元素相关的辅助类 private Messager mMessager; //日志相关的辅助类 @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mFiler = processingEnv.getFiler(); mElementUtils = processingEnv.getElementUtils(); mMessager = processingEnv.getMessager(); mAnnotatedClassMap = new TreeMap<>(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return false; } /** * 指定使用的 Java 版本。通常返回SourceVersion.latestSupported()。 * @return */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * 指定哪些注解应该被注解处理器注册 * @return */ @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>(); types.add(BindView.class.getCanonicalName()); types.add(OnClick.class.getCanonicalName()); return types; } }
用
@AutoService来注解这个处理器,可以自动生成配置信息。
在 init() 可以初始化拿到一些实用的工具类。
这里涉及到了
Element元素,借用一下别人的分析:
这个类的的基本内容就完成了,
现在创建
BindViewField类,来解析
BindView注解类来获取用该注解的相关信息
/** * Created by JokAr on 16/8/8. */ public class BindViewField { private VariableElement mVariableElement; private int mresId; 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())); } mVariableElement = (VariableElement) element; BindView bindView = mVariableElement.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(), mVariableElement.getSimpleName())); } } /** * 获取变量名称 * @return */ public Name getFieldName() { return mVariableElement.getSimpleName(); } /** * 获取变量id * @return */ public int getResId() { return mresId; } /** * 获取变量类型 * @return */ public TypeMirror getFieldType() { return mVariableElement.asType(); } }
创建
OnClickMethod类来解析使用
OnClick注解的方法,获取相关信息
public class OnClickMethod { private ExecutableElement mExecutableElement; private int[] resIds; private Name mMethodName; public OnClickMethod(Element element) throws IllegalArgumentException { if (element.getKind() != ElementKind.METHOD) { throw new IllegalArgumentException( String.format("Only methods can be annotated with @%s", OnClick.class.getSimpleName())); } mExecutableElement = (ExecutableElement) element; resIds = mExecutableElement.getAnnotation(OnClick.class).value(); if (resIds == null) { throw new IllegalArgumentException(String.format("Must set valid ids for @%s", OnClick.class.getSimpleName())); } else { for (int id : resIds) { if (id < 0) { throw new IllegalArgumentException(String.format("Must set valid id for @%s", OnClick.class.getSimpleName())); } } } mMethodName = mExecutableElement.getSimpleName(); List<? extends VariableElement> parameters = mExecutableElement.getParameters(); if (parameters.size() > 0) { throw new IllegalArgumentException( String.format("The method annotated with @%s must have no parameters", OnClick.class.getSimpleName())); } } /** * 获取方法名称 * @return */ public Name getMethodName() { return mMethodName; } /** * 获取id数组 * @return */ public int[] getResIds() { return resIds; } }
然后重点就是生成Java代码文件的类:
/** * Created by JokAr on 16/8/8. */ public class AnnotatedClass { private TypeElement mTypeElement; private ArrayList<BindViewField> mFields; private ArrayList<OnClickMethod> mMethods; private Elements mElements; public AnnotatedClass(TypeElement typeElement, Elements elements) { mTypeElement = typeElement; mElements = elements; mFields = new ArrayList<>(); mMethods = new ArrayList<>(); } public String getFullClassName() { return mTypeElement.getQualifiedName().toString(); } public void addField(BindViewField field) { mFields.add(field); } public void addMethod(OnClickMethod method) { mMethods.add(method); } public JavaFile generateFile() { //generateMethod MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(mTypeElement.asType()), "host", Modifier.FINAL) .addParameter(TypeName.OBJECT, "source") .addParameter(TypeUtil.PROVIDER,"provider"); for(BindViewField field : mFields){ // find views injectMethod.addStatement("host.$N = ($T)(provider.findView(source, $L))", field.getFieldName(), ClassName.get(field.getFieldType()), field.getResId()); } for(OnClickMethod method :mMethods){ 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(); injectMethod.addStatement("View.OnClickListener listener = $L ", listener); for (int id : method.getResIds()) { // set listeners injectMethod.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id); } } //generaClass TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewInject") .addModifiers(Modifier.PUBLIC) .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJET, TypeName.get(mTypeElement.asType()))) .addMethod(injectMethod.build()) .build(); String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString(); return JavaFile.builder(packgeName, injectClass).build(); } }
具体的可以看javapoet的API,然后我们需要完善
ViewInjectProcesser类,增加:
private Map<String, AnnotatedClass> mAnnotatedClassMap; @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { mAnnotatedClassMap.clear(); try { processBindView(roundEnv); processOnClick(roundEnv); } catch (IllegalArgumentException e) { e.printStackTrace(); error(e.getMessage()); } for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) { try { annotatedClass.generateFile().writeTo(mFiler); } catch (IOException e) { error("Generate file failed, reason: %s", e.getMessage()); } } return true; } private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException { for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField bindViewField = new BindViewField(element); annotatedClass.addField(bindViewField); } } private void processOnClick(RoundEnvironment roundEnv) throws IllegalArgumentException { for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); OnClickMethod onClickMethod = new OnClickMethod(element); annotatedClass.addMethod(onClickMethod); } } private void error(String msg, Object... args) { mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args)); }
实际使用
Android Stduio 2.2以下使用方法
在项目的根目录的build.gradle添加:classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
在项目的主module的build.gradle添加:
apply plugin: 'com.neenbedankt.android-apt' compile project(':viewinject-annotation') compile project(':inject') apt project(':viewinject-compiler')
Android Stduio 2.2以上使用方法
compile project(':viewinject-annotation') compile project(':inject') annotationProcessor project(':viewinject-compiler')
在自己的activity类使用:
/** * Created by JokAr on 16/8/8. */ public class MainActivity extends AppCompatActivity { @BindView(R.id.textView) TextView textView; @BindView(R.id.button1) Button button1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ViewInject.inject(this); } @OnClick(R.id.textView) public void click() { Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show(); } }
点击makeProject 就编译完成后就可以在主项目module的/build/generated/source/apt/debug 目录下看到生成的java类文件了
一个学习级的apt项目就完成了。
项目源码
实战项目:Android6.0权限管理 工具,我用java重写别人的kotlin项目;地址:
https://github.com/a1018875550/PermissionDispatcher
相关文章推荐
- 1.Android注解-编译时生成代码 APT(Annotation Processing Tool ) Poet 说明
- 2.Android注解-编译时生成代码 APT(Annotation Processing Tool ) 实例说明
- apt 根据注解,编译时生成代码
- Android 编译时注解生成代码
- android apt编译时期自动生成代码
- Android APT(编译时代码生成)最佳实践
- Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架
- 使用编译时注解annotationProcessor(取代android-apt)开发组件化路由框架
- Android注解使用之注解编译android-apt如何切换到annotationProcessor
- Android APT开发教程 四 apt生成代码与所注释元素之间的交互
- 深入理解编译注解(二)annotationProcessor与android-apt
- 工欲善其事,必先利其器 软件工具开发关键词 protractor自动化测试工具 RegexBuddy正则 CodeSmith,LightSwitch:代码生成 CheatEngine:玩游戏修改内存值必备神器 ApkIDE:Android反编译工具 Reflector:反编译dll动态链接库
- Android APT开发教程 四 apt生成代码与所注释元素之间的交互
- 【Android】打包过程:生成自动代码->编译->(混淆)->dex文件->生成资源文件->打apk包->(签名)->对齐
- Java编译时注解自动生成代码
- eclipse编译android开源代码示范,及生成jar引用出错原因解析
- Android 利用 APT 技术在编译期生成代码
- Android 利用 APT 技术在编译期生成代码
- Android注解使用之注解编译android-apt如何切换到annotationProcessor
- Android 利用 APT 技术在编译期生成代码