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

使用编译时注解实现简易的 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));
		}
	}
}

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值