Android注解-编译时生成代码 (APT)

Android注解越来越引领潮流,比如 Dagger2, ButterKnife, EventBus3 等,他们都是注解类型,而且他们都有个共同点就是编译时生成代码,而不是运行时利用反射,这样大大优化了性能;而这些框架都用到了同一个工具就是:APT(Annotation Processing Tool ),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。

今天我们要自己实现的就是类似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

你可能感兴趣的:(android,apt)