使用编译时注解实现简易的 ButterKnife 效果

一、前言

现在有太多了关于注解的三方框架供我们使用,比如 ButterKnife、Dagger2 等,我们不仅要会使用,还要知道其中的大致原理。接下来就通过一个小的实例来熟悉下编译时注解。

关于注解的基础知识就不介绍的,有兴趣的可以去看看这篇文章,写的挺好的。

另外本例子中的代码已经传到我的 Github,有兴趣的可以去看下。

二、准备

1、 新建项目 BindView

2、 新建 Module,命名为 annotation,类型选择为 Java Library。

3、 新建 Module,命名为 compile,类型选择为 Java Library。
在该 Module 的 build.gradle 中添加:

compile project(':annotation')
compile 'com.google.auto.service:auto-service:1.0-rc3'

4、 新建 Module,命名为 api,类型选择为 Android Library。
在该 Module 的 build.gradle 中添加 :

compile project(':annotation')

5、 在 app Module的 build.gradle 中添加:

compile project(':api')
annotationProcessor project(':compile')

三、编码实现

3.1 编写注解

在 annotation 模块中新建注解如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

3.2 编写注解处理器

在 compile 模块中新建类 BindViewProcessor 继承于 AbstractProcessor,并添加 @AutoService(Processor.class) 注解。

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    /**
     * 代码写入工具
     */
    private Filer mFiler;
    /**
     * 日志打印
     */
    private Messager mMessager;
    /**
     * 跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
     */
    private Elements mElementUtils;

    private Map<String, ProxyInfo> mProxyInfoCacheMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //获取日志打印工具
        mMessager = processingEnv.getMessager();
        //获取元素辅助类
        mElementUtils = processingEnvironment.getElementUtils();
        //获取代码写入工具
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 新建 LinkedHashMap 用于存储有序的注解名称集合
        Set<String> annotationTypes = new LinkedHashSet<>();
        // 添加我们的BindView到集合中
        annotationTypes.add(BindView.class.getCanonicalName());
        return annotationTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 指定Java版本,一般返回 latestSupported();
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //...
        return false;
    }

}    

前面是准备工作,接下来就要编写最重要的 process 方法:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // 每次清除缓存
    mProxyInfoCacheMap.clear();
    // 返回被BindView注释的元素
    Set<? extends Element> elementsWithBindView = roundEnvironment.getElementsAnnotatedWith(BindView.class);
    //遍历被BindView注解的元素
    for (Element element : elementsWithBindView) {
        // 检查注解有效性
        checkAnnotationValid(element, BindView.class);
        // 获取成员变量元素
        VariableElement variableElement = (VariableElement) element;
        // 获取成员变量元素所在类元素
        TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
        // 获取类的全称
        String fullClassName = classElement.getQualifiedName().toString();
        // 从缓存中取出编译生成类信息
        ProxyInfo proxyInfo  = mProxyInfoCacheMap.get(fullClassName);
        // 如果缓存中没有,就新建,然后添加到缓存中
        if (proxyInfo == null){
            proxyInfo = new ProxyInfo(mElementUtils,classElement);
            mProxyInfoCacheMap.put(fullClassName,proxyInfo);
        }
        // 获取注解类对象
        BindView bindViewAnnotation = variableElement.getAnnotation(BindView.class);
        // 获取注解的中value的值
        int id = bindViewAnnotation.value();
        // 在编译生成类中存储元素
        proxyInfo.injectVariables.put(id,variableElement);
    }
    // 遍历缓存中的所有元素生成编译类
    for (String s : mProxyInfoCacheMap.keySet()) {
        // 缓存中取出ProxyInfo
        ProxyInfo proxyInfo = mProxyInfoCacheMap.get(s);
        try {
            // 生成代码
            JavaFileObject jfo = mFiler.createSourceFile(proxyInfo.getProxyClassFullName(),proxyInfo.getTypeElement());
            Writer writer = jfo.openWriter();
            writer.write(proxyInfo.generatedJavaCode());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
            error(proxyInfo.getTypeElement(),"Unable to write injector for type %s: %s",
                    proxyInfo.getTypeElement(),e.getMessage());
        }
    }
    return true;
}

/**
 * 检查注解是否可用
 *
 * @param annotatedElement
 * @param clazz
 * @return
 */
private boolean checkAnnotationValid(Element annotatedElement, Class clazz) {
    if (annotatedElement.getKind() != ElementKind.FIELD) {
        error(annotatedElement, "%s must be declared on field.", clazz.getSimpleName());
        return false;
    }
    if (ClassUtils.isPrivate(annotatedElement)) {
        error(annotatedElement, "%s() must can not be private.", annotatedElement.getSimpleName());
        return false;
    }
    return true;
}

/**
 * 错误日志打印
 *
 * @param element
 * @param message
 * @param args
 */
private void error(Element element, String message, Object... args) {
    if (args.length > 0) {
        message = String.format(message, args);
    }
    mMessager.printMessage(Diagnostic.Kind.NOTE, message, element);
}

这里就不在说明了,注释已经很详细了。

上面使用到的类如下:

ProxyInfo :

public class ProxyInfo {
    /**
     * 包名
     */
    private String mPackageName;
    /**
     * 类名
     */
    private String mClassName;
    /**
     * 类或接口程序元素
     */
    private TypeElement mTypeElement;
    /**
     * 用于存储id和元素的HashMap
     */
    public Map<Integer, VariableElement> injectVariables = new HashMap<>();

    /**
     * 构造方法中初始化成员变量
     * @param elements
     * @param classElement
     */
    public ProxyInfo(Elements elements, TypeElement classElement) {
        this.mTypeElement = classElement;
        //获取包名
        PackageElement packageElement = elements.getPackageOf(classElement);
        String packageName = packageElement.getQualifiedName().toString();
        //获取类名
        String className = ClassUtils.getClassName(classElement, packageName);
        this.mClassName = className+"$$"+Constants.SUFFIX;
        this.mPackageName = packageName;
    }

    /**
     * 生成Java代码
     * @return
     */
    public String generatedJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("//Generated code,Do not modify!")
                .append("\npackage ").append(mPackageName).append(";\n\n")
                .append("import cn.smartsean.api.*;\n\n")
                .append("public class ").append(mClassName).append(" implements " + Constants.SUFFIX + "<" + mTypeElement.getQualifiedName() + ">{\n\n");
        generatedMethods(builder);
        builder.append("\n}\n");
        return builder.toString();
    }

    /**
     * 生成方法
     * @param builder
     */
    private void generatedMethods(StringBuilder builder) {
        builder.append("\t@Override\n")
                .append("\tpublic void inject(" + mTypeElement.getQualifiedName() + " host,Object source) { \n ");
        for (Integer integer : injectVariables.keySet()) {
            VariableElement element = injectVariables.get(integer);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append("\t\tif (source instanceof android.app.Activity){ \n")
                    .append("\t\t\thost." + name + " = (" + type + ")(((android.app.Activity)source).findViewById(" + integer + "));\n")
                    .append("\t\t}else {\n")
                    .append("\t\t\thost." + name + " = (" + type + ")(((android.view.View)source).findViewById(" + integer + "));\n")
                    .append("\t\t}");
        }
        builder.append("\n\t}");
    }

    /**
     * 获取全称
     * @return
     */
    public String getProxyClassFullName() {
        return this.mPackageName + "." + this.mClassName;
    }

    /**
     * 获取类型元素
     * @return
     */
    public TypeElement getTypeElement() {
        return mTypeElement;
    }
}

ClassUtils :

public class ClassUtils {
    /**
     * 是否是私有的
     * @param annotatedClass
     * @return
     */
    public static boolean isPrivate(Element annotatedClass) {
        return annotatedClass.getModifiers().contains(PRIVATE);
    }

    /**
     * 类名
     * @param type
     * @param packageName
     * @return
     */
    public static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen)
                .replace('.', '$');
    }
}

Constants :

public class Constants {
    public static final String SUFFIX = "ViewInject";
}

至此,注解处理器已经完成了,接来下看看 api 模块

3.3 编写 api 模块

首先定义 注入接口:

public interface ViewInject<T> {
    void inject(T t,Object o);
}

然后编写注入工具类:BindView

public class BindView {
    public static final String SUFFIX = "$$ViewInject";
    /**
     * 在Activity中使用
     * @param activity
     */
    public static void bind(Activity activity) {
        //根据传入的activity得到viewInject对象,并注入
        ViewInject viewInject = findProxyActivity(activity);
        viewInject.inject(activity, activity);
    }

    public static void bind(Object object, View view) {
        ViewInject viewInject = findProxyActivity(object);
        viewInject.inject(object, view);
    }

    /**
     * 查找我们生成的类
     * @param object
     * @return
     */
    private static ViewInject findProxyActivity(Object object) {
        try {
            Class clz = object.getClass();
            Class bindViewClass = Class.forName(clz.getName() + SUFFIX);
            return (ViewInject) bindViewClass.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        throw new RuntimeException(String.format("can not find %s , something when compiler.", object.getClass().getSimpleName() + SUFFIX));
    }
}

四、使用

在 MainActivity 中使用:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.test)
    TextView mTest;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        cn.smartsean.api.BindView.bind(this);
        mTest.setText("This is my first annotation processor test");
    }
}

先 build 下项目,在 app-build-generated-source-apt-cn.smartsean.bindview文件夹下有个 MainActivity$$ViewInject 文件。

这个文件就是我们通过编译时注解生成的,用来 findViewById 的。

MainActivity$$ViewInject:

//Generated code,Do not modify!
package cn.smartsean.bindview;

import cn.smartsean.api.*;

public class MainActivity$$ViewInject implements ViewInject<cn.smartsean.bindview.MainActivity>{

    @Override
    public void inject(cn.smartsean.bindview.MainActivity host,Object source) { 
        if (source instanceof android.app.Activity){ 
            host.mTest = (android.widget.TextView)(((android.app.Activity)source).findViewById(2131165305));
        }else {
            host.mTest = (android.widget.TextView)(((android.view.View)source).findViewById(2131165305));
        }
    }
}

接下来运行下程序,效果图如下:

191524035752_.pi

本例子中的代码已经传到我的 Github,有兴趣的可以去看下。

猜你喜欢

转载自blog.csdn.net/Sean_css/article/details/79991112
今日推荐