尝试手写一个注解框架

前两天在安科网上看到一篇文章: 一个小需求引发的思考。
需求是根据多个EditText是否有输入的值来确定Button是否可点击。很常见的一个需求吧,但是要怎么做到简单优雅呢?文章里也有讲到封装的过程,这里我就不细讲了。最后作者封装成了用注解就可以做到。但是他使用的是反射技术,反射技术会影响App的性能。大家都知道著名的注解框架 butterknife是使用了动态生成java代码的技术,这对性能影响非常小,我就在想我是不是也可以试试呢,正好学习一下butterknife原理以及用到的技术。于是说干就干,就有了下面的尝试!(我也是在查阅了许多资料后学习中摸索的,如果有写的不对的地方请大神指正。另外第一次写博客,排版什么的可能不是很美观。哈哈哈,想把自己的学习心得分享出来。慢慢进步吧!)


首先要知道butterknife使用了什么技术,那就得阅读源码,网上搜一搜文章一大堆哈。这就不废话了哈哈哈。其实最重要的技术点就两个:

  • 怎么解析处理注解
  • 怎么动态生成java代码文件

阅读源码得知前者使用了 AndroidAnnotations框架,后者则使用了Javapoet框架。这里简单介绍一下吧。

AndroidAnnotations

AndroidAnnotations是一个javax的注解解析技术。我们可以通过继承javax.annotation.processing.AbstractProcessor这个类来定义一个自己的注解处理类。(由于Android已经不支持javax编程了,所以需要在一个java lib 中来写)。

@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {


    /**
     * 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,
     * 并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    /**
     * 这相当于每个处理器的主函数main()。 在这里写扫描、评估和处理注解的代码,以及生成Java文件。
     * 输入参数RoundEnviroment,可以让查询出包含特定注解的被注解元素。
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    /**
     * 这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,
     * 包含本处理器想要处理的注解类型的合法全称。换句话说,在这里定义你的注解处理器注册到哪些注解上。
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    /**
     * 返回支持的java版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}

上面的代码中通过注释应该了解每一个方法的作用了。我们处理的主要方法就是process()方法了。
但是这个类上面还有一个注解@AutoService(Processor.class)
它是干嘛的呢?是这样的:在以往的定义注解解析器时,需要在解析器类定义过程中,做以下操作:
在解析类名前定义:
@SupportedAnnotationTypes("com.bosssoft.cloin.ViewInjectProcesser")
@SupportedSourceVersion(SourceVersion.RELEASE_7)

同时在java的同级目录新建resources目录,新建META-INF/services/javax.annotation.processing.Processor文件,文件中填写你自定义的Processor全类名,这是向JVM声明解析器。

当然幸好我们现在使用的是AndroidStudio,可以用auto-service来替代以上操作。只要在注解类前面加上@AutoService(Processor.class)就可以替代以上操作。它是由谷歌开发的,在gradle中加上:

compile 'com.google.auto.service:auto-service:1.0-rc2'

有兴趣的小伙伴可以自行网上搜索了解更多内容....


Javapoet

Javapoet是squareup公司提供的能够自动生成java代码的库。

讲一下javapoet里面常用的几个类:
MethodSpec代表一个构造函数或方法声明。
TypeSpec 代表一个类,接口,或者枚举声明。
FieldSpec 代表一个成员变量,一个字段声明。
JavaFile包含一个顶级类的Java文件

举个例子:

private void generateHelloworld() throws IOException{
        //构建main方法
        MethodSpec main = MethodSpec.methodBuilder("main") //main代表方法名
                    .addModifiers(Modifier.PUBLIC,Modifier.STATIC)//Modifier 修饰的关键字
                .addParameter(String[].class, "args") //添加string[]类型的名为args的参数
                    .addStatement("$T.out.println($S)", System.class,"Hello World")//添加代码,这里其实就是添加了System,out.println("Hello World");
                .build();
                //构建HelloWorld类
                TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")//HelloWorld是类名
                .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
                .addMethod(main)  //在类中添加方法
                .build();
                //生成java文件
        JavaFile javaFile = JavaFile.builder("com.example.helloworld", typeSpec)
                .build();
        javaFile.writeTo(System.out);
    }

运行一下

尝试手写一个注解框架

看到这应该知道java代码是怎么生成的了吧。现在需要用到的技术点都大致了解了。准备工作做好了,现在进入正题吧。

项目结构

尝试手写一个注解框架

annotation:(java lib) 提供注解。
annotation-compiler:(java lib)注解处理。
annotation-api:(Android Lib) 是我们外部用到 api。
app:是调用api进行测试的。

分别讲解

APP模块中测试

public class MainActivity extends AppCompatActivity {

    @WatchEdit(editIds = {R.id.ed_1, R.id.ed_2, R.id.ed_3})
    Button button1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button1 = (Button) findViewById(R.id.bbb11);
        ViewAnnoUtil.watch(this);
        button1.setEnabled(false);
    }
}

看看我们使用了什么:
一个注解标记:@WatchEdit
还有一句代码: ViewAnnoUtil.watch(this);

这两句话到底是怎么生效的呢?
先来看这个注解标记

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface WatchEdit {

    /**
     * 被观察的输入框的id
     *
     * @return
     */
    int[] editIds();
}

它位于annotation模块中,为了观察多个EditText,定义一个注解。参数是 要观察的EditText的id。

再来看ViewAnnoUtl.watch(this)干了啥:

//最终对外使用的工具类
public class ViewAnnoUtil {
    private static ActivityWatcher actWatcher = new ActivityWatcher();

    private static Map<String, Injector> WATCH_MAP = new HashMap<>();

    public static void watch(Activity activity) {
        watch(activity, activity, actWatcher);
    }

    private static void watch(Object host, Object source, Watcher watcher) {
        String className = host.getClass().getName();
        try {
            Injector injector = WATCH_MAP.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                WATCH_MAP.put(className, injector);
            }
            injector.inject(host, source, watcher);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

看到这个类中的代码,我们知道,watch(this)实际上调用了watch(Object host, Object source, Watcher watcher)方法。

其中hostsource参数都是传的activity,watcher参数则是传的类里实例化的ActivityWatcher对象实例。做了一些由于使用这里使用了一些反射,所以通过使用内存缓存来进行优化。最后则调用了injector.inject()方法,那我们看看这些都是什么东西。

//观察类的接口
public interface Watcher {
    /**
     * 查找view的方法
     *
     * @param obj view的来源,哪个activity或者fragment
     * @param id  要查找的view的id
     * @return 查找到的view
     */
    EditText findView(Object obj, int id) throws ClassCastException;

    /**
     * 进行观察
     *
     * @param editText 被观察的edit
     * @param obser    观察的view
     */
    void watchEdit(EditText editText, View obser);
}
//提供一个默认的通过Activity实现的Watcher
public class ActivityWatcher implements Watcher, TextWatcher {
    private HashMap<View, ArrayList<EditText>> map = new HashMap<>();


    @Override
    public EditText findView(Object obj, int id) throws ClassCastException {
        return (EditText) ((Activity) obj).findViewById(id);
    }

    @Override
    public void watchEdit(EditText editText, final View obser) {
        if (map.get(obser) == null) {
            ArrayList<EditText> itemEditList = new ArrayList<>();
            itemEditList.add(editText);
            map.put(obser,itemEditList);
        } else {
           map.get(obser).add(editText);
        }
        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                if (checkEnable(map.get(obser)))
                    obser.setEnabled(true);
                else obser.setEnabled(false);
            }
        });


    }

    private boolean checkEnable(ArrayList<EditText> editList) {
        for (EditText text : editList) {
            if (TextUtils.isEmpty(text.getText().toString()))
                return false;
        }
        return true;
    }

这很好理解了吧,也就是说具体的让Button监听到EditText输入变化的代码在这里。
再来看injector:

//绑定的接口类
public interface Injector<T> {
    /**
     * @param host    目标
     * @param source  来源
     * @param watcher 提供具体使用的方法 查找edit,添加监听
     */
    void inject(T host, Object source, Watcher watcher);
}

可以发现其实它是一个接口,规定了目标从哪里来,由谁来执行这个监听操作(Wathcer)
那么问题来了,光是接口怎么能够实现功能呢?肯定得有一个接口的实现类才行吧。
别着急,我们看这一段代码:

String className = host.getClass().getName();
 Injector injector = WATCH_MAP.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                WATCH_MAP.put(className, injector);
            }

可以发现其实我们用反射加载了一个类,类名是 host的类名+ "$$Injector"
是不是很熟悉?使用butterknife的小伙伴肯定遇到过 MainActivity&&ViewBinder这类似的类名吧。没错就是它。他就是我们 Injector的实现类,完成了具体的实现。只是它是由我们前面提到的 javapoet动态生成的。再来看看这个顺序:

ViewAnnoUtil.watch() ----> injector.inject()并传入了目标的Activity,和我们写好的ActivityWacther。
通过动态生成的injector实现类来协调。

现在我们来看看怎么生成这个实现类。

annotation-compiler中则包含注解处理器,java文件生成等

//常量工具类
public class TypeUtil {
    public static final ClassName WATCHER = ClassName.get("com.colin.annotation_api", "Watcher");

    public static final ClassName INJECTOR = ClassName.get("com.colin.annotation_api", "Injector");
}

注解处理类

@AutoService(Processor.class)
public class WatchEditProcessor extends AbstractProcessor {
    //具体代码我放后面
}

前面介绍过怎么定义注解处理器,我们来看看这个类里该干什么

先来简单的,定义我们支持的注解,我们这只支持@WatchEdit这个注解。(可以有多个)

@Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();

        types.add(WatchEdit.class.getCanonicalName());
        return types;
    }

我们支持的java版本是最高版本

@Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

定义几个成员变量:

//文件工具类
    private Filer mFiler; 

    //处理元素的工具类
    private Elements mElementUtils;

    //log工具类
    private Messager mMessager; 

    //使用了注解的类的包装类的集合
    private Map<String, WatchEditAnnotatedClass> mAnnotatedClassMap = new HashMap<>();

然后在init方法中进行了初始化

@Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElementUtils = processingEnvironment.getElementUtils();
        mMessager = processingEnvironment.getMessager();
        mFiler = processingEnvironment.getFiler();
    }

最后看最重要的方法 process()

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        mAnnotatedClassMap.clear();
        try {
            processWatchEdit(roundEnvironment);
        } catch (IllegalArgumentException e) {
            error(e.getMessage());
            return true;
        }

        try {

            for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
                info("generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateWatcher().writeTo(mFiler);
            }
        } catch (Exception e) {
            e.printStackTrace();
            error("Generate file failed,reason:%s", e.getMessage());
        }
        return true;
    }

返回true表示已经处理过。

先看这句processWatchEdit(roundEnvironment);代码干了什么:

private void processWatchEdit(RoundEnvironment roundEnv) {
        //遍历处理 使用了 @WatchEdit 注解的类
        //一个element代表一个元素(可以是类,成员变量等等)
        for (Element element : roundEnv.getElementsAnnotatedWith(WatchEdit.class)) {
            WatchEditAnnotatedClass annotatedClass = getAnnotatedClass(element);
            //通过 roundEnv工具构建一个成员变量
            WatchEditField field = new WatchEditField(element);
            //添加使用了@WatchEdit注解的成员变量
            annotatedClass.addField(field);
        }
    }


    private WatchEditAnnotatedClass getAnnotatedClass(Element element) {
        //得到一个 类元素
        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
        //拿到类全名
        String fullClassName = encloseElement.getQualifiedName().toString();
        //先从缓存中取
        WatchEditAnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
        if (annotatedClass == null) {
            //没有就构建一个
            annotatedClass = new WatchEditAnnotatedClass(encloseElement, mElementUtils);
            //放入缓存
            mAnnotatedClassMap.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }

这里又用到了两个类:
WatchEditField:被@WatchEdit注解标记的成员变量的包装类。
WatchEditAnnotatedClass:使用了@WatchEdit注解的类。
拿上面的例子来说MainActivity就是WatchEditAnnotatedClass
Button button1这个button1就是WatchEditField
这两个类里面具体有什么待会看,现在看下一段代码:

for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
                info("generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateWatcher().writeTo(mFiler);
            }

这里循环我们的包装类,并调用generateWatcher()方法,并写入到前面提到的文件工具中。 看这个方法名就知道,这里就是生成java代码的核心方法了。至此流程终于连上了。。。不容易啊 =_=

梳理一下:

尝试手写一个注解框架

流程搞明白了,接下来看看,我们费了大力气生成的java文件怎么生成的,也就是generateWatcher()里做了啥,来看代码:

public JavaFile generateWatcher() {
        String packageName = getPackageName(mClassElement);
        String className = getClassName(mClassElement, packageName);
        //获取到当前使用了注解标记的类名(MainActivity)
        ClassName bindClassName = ClassName.get(packageName, className);
        //构建出重写的inject方法
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.WATCHER, "watcher");

        //添加代码
        for (WatchEditField field : mFiled) {
            //获得每个button要监听的EditText的id
            int[] ids = field.getResIds();
            if (ids != null) {
                //为每个EditText添加监听
                for (int i = 0; i < ids.length; i++) {
                    //添加监听
                    methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
                            ids[i], "host." + field.getFieldName());
//                    methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
//                            ids[i], field);
                }
            }
        }

        //构建类 MainActivity$$Injector
        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(mClassElement.asType())))
                .addMethod(methodBuilder.build()) //添加刚刚生成的injector方法
                .build();
        //生成一个java文件
        return JavaFile.builder(packageName, finderClass).build();
    }

这里就用到了我们前面提到的javapoet库了。通过注释应该很好理解这段代码的意思了。
关于javapoet有兴趣的小伙伴可以自行搜索了解更多内容。

好了,至此一切都结束了!!!至于WatchEditField的代码贴下面了。
测试了一波功能是正常运行了。。不会贴动图。。。然后接下来就要考虑的是接触绑定,释放资源等等优化了。先到这吧。平时用起来很方便的东西了解一下原理才发现还是很复杂的。有学习才有进步。加油。另外Demo我会放到Github上去。里面也会慢慢更新出更多的东西
GitHub

被@WatchEdit注解标记的成员变量包装类,如一个 button

public class WatchEditField {
    private VariableElement mFieldElement;

    private int[] mEditIds;

    public WatchEditField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
                    WatchEdit.class.getSimpleName()));
        }
        mFieldElement = (VariableElement) element;

        WatchEdit bindView = mFieldElement.getAnnotation(WatchEdit.class);
        if (bindView != null) {
            mEditIds = bindView.editIds();
            if (mEditIds == null && mEditIds.length <= 0) {
                throw new IllegalArgumentException(String.format("editIds() in %s for field % is not valid",
                        WatchEdit.class.getSimpleName(), mFieldElement.getSimpleName()));
            }
        }
    }


    public Name getFieldName() {
        return mFieldElement.getSimpleName();
    }


    public int[] getResIds() {
        return mEditIds;
    }

    public TypeMirror getFieldType() {
        return mFieldElement.asType();
    }

    public Object getConstantValue(){
        return mFieldElement.getConstantValue();
    }
}

相关推荐