zoukankan      html  css  js  c++  java
  • 手写ButterKnife

    开发中使用注解框架可以极大地提高编码效率,注解框架用到的技术可以分为两种,运行时注解跟编译时注解。运行时注解一般配合反射机制使用,编译时注解则是用来生成模板代码。这里我们分别使用这两种方法实现ButterKnife的控件绑定功能。

    1、运行时注解

    运行时注解实现比较简单,但是由于完全依靠反射技术,所以运行效率较低。首先我们需要新建一个注解类,指定其保留时间为运行时,修饰对象为类成员变量,值为控件ID。

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

    然后我们需要使用反射实现 bindView 方法。先是使用 class.getDeclaredFields 方法获取类中所有的成员变量,如果该成员变量被 @BindView 注解则根据注解中的控件ID调用 view.findViewById ,并将返回值赋给该成员变量。这种方法的一个好处是被 @BindView 注解的成员变量可以是私有的。

    public class ButterKnife {
        public static void bindView(Activity activity) {
            bindView(activity, activity.getWindow().getDecorView());
        }
        public static void bindView(Object object, View view) {
            Class clazz = object.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field f : fields) {
                BindView binder = f.getAnnotation(BindView.class);
                if (binder != null) {
                    f.setAccessible(true);
                    f.set(object, view.findViewById(binder.value()));
                }
            }
        }
    }
    

    2、编译时注解

    我们知道ButterKnife是基于编译时注解的,依赖注解生成模板代码从而实现控件绑定。使用编译时注解前需要对注解处理器(Annotation Processor)有一个基本了解。

    Annotation processing is a tool build in javac for scanning and processing annotations at compile time. You can register your own annotation processor for certain annotations. 

    在这里我们通过注解处理器自动生成模板代码,并且与项目代码一起编译。

    2.1、创建库项目 

    在Android Studio中创建完项目后,我们需要在 File -> New -> New Module... 创建3个模块。

    名称 类别 作用
    test-annotations Java Liabrary 存放我们自己定义的注解类
    test-api Android Liabrary 存放在主模块中使用的工具类
    test-compiler Java Liabrary 存放注解处理器

    然后需要在每个模块的build.gradle文件中配置它们之间的依赖关系以及需要用到的类库。

    // test-annotations:
    dependencies {
        ...
        implementation 'com.android.support:support-annotations:27.1.0@jar'
    }
    
    // test-api:
    dependencies {
        ...
        compile project(path: ':test-annotations')
    }
    
    // test-compiler:
    dependencies {
        ...
        compile 'com.google.auto.service:auto-service:1.0-rc2'
        compile project(path: ':test-annotations')
    }

    2.2、自定义注解

    接下来我们需要在“test-annotations”模块中创建一个用于控件绑定的注解类,与第一节中类似,不过保留时间更改为了编译时。

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

    2.3、创建注解处理器

    处理器的实现在“test-compiler”模块中,代码比较繁琐,大体上分为两步:收集信息然后生成代码。

    @AutoService(Processor.class)
    public class TestProcessor extends AbstractProcessor {
    
        private Filer mFileUtils;
        private Elements mElementUtils;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            mFileUtils = processingEnvironment.getFiler();
            mElementUtils = processingEnvironment.getElementUtils();
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> annotationType = new HashSet<>();
            annotationType.add(BindView.class.getCanonicalName());
            return annotationType;
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            // 处理逻辑...
        }
    }
    

    这里使用了谷歌提供的 @AutoService 对处理器进行注册,然后复写AbstractProcessor类的方法。在 getSupportedAnnotationTypes 函数中返回处理器支持的注解类型,在 init 函数中获取我们需要用到的工具类,处理器的主要逻辑则在 process 函数中。

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Map<String, ClassInfo> classMap = new HashMap<>();
        // 收集信息:
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AnnotationValue.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.FIELD) {
                VariableElement variableElement = (VariableElement) element;
                // 获取成员变量所在的类。
                TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
                // 获取类的全限定名。
                String qualifeiedName = typeElement.getQualifiedName().toString();
                // 将同一个类的所有成员变量放到一个ClassInfo类中
                ClassInfo classInfo = classMap.get(qualifeiedName);
                if (classInfo == null) {
                    PackageElement packageElement = mElementUtils.getPackageOf(typeElement);
                    classInfo = new ClassInfo(packageElement, typeElement);
                    classMap.put(qualifeiedName, classInfo);
                }
                classInfo.addVariable(variableElement); 
            }
        }
        // 生成代码:
        ...
    }

    首先我们通过 roundEnvironment.getElementsAnnotatedWith 获取到被 @BindView 注解的所有元素,显然这里的 @BindView 希望注解在类的成员变量上,所以先对元素的类型进行判断,然后将所有的元素按照所在类的全限定名进行分类(通过ClassInfo类表示)。这里需要了解Element类的含义:

    • VariableElement:一般代表成员变量。
    • ExecutableElement:一般代表类中的方法。
    • TypeElement:一般代表类。
    • PackageElement:一般代表包。 
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 收集信息:
        ...
        // 生成代码:
        for (String key : classMap.keySet()) {
            ClassInfo classInfo = classMap.get(key);
            try {
                JavaFileObject sourceFile = mFileUtils.createSourceFile(classInfo.getProxyClassFullName());
                Writer writer = sourceFile.openWriter();
                writer.write(classInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }
    

    代码生成阶段比较简单,先使用 mFileUtils.createSourceFile 函数创建文件对象,文件名通过 classInfo.getProxyClassFullName函数获得。然后向该文件对象中写入通过 classInfo.generateJavaCode 函数获取的源码。接下来看一下ClassInfo类的实现:

    public class ClassInfo {
        private String mPackageName;
        private String mProxyClassName;
        private ArrayList<VariableElement> mInjectVariables = new ArrayList<>();
        public static final String PROXY = "ViewInject";
    
        public ClassInfo(PackageElement packageElement, TypeElement classElement) {
            // 获取包名
            String packageName = packageElement.getQualifiedName().toString();
            this.mPackageName = packageName;
            // 获取全限定名后去除包名,将‘.’替换为‘$’是因为如果类A有个内部
            // 类B,那么B在编译后名字会变为A.B
            int packageLen = packageName.length() + 1;
            String className = classElement.getQualifiedName().toString().substring(packageLen).replace('.', '$');
            // 设置代理类的名字,之后我们需要根据这个名字获取到代理类。
            this.mProxyClassName = className + "$$" + PROXY;
        }
        public String generateJavaCode() {  
            StringBuilder builder = new StringBuilder();
            builder.append(String.format("package %s;
    ", mPackageName));
            builder.append(String.format("public final class %s {
    ", mProxyClassName));
            builder.append(String.format("public %s(%s obj) {
    ", mProxyClassName, mClassName));
            for (VariableElement element : mInjectVariables) {
                AnnotationValue binder = element.getAnnotation(AnnotationValue.class);
                String name = element.getSimpleName().toString();
                builder.append(String.format("obj.%s=obj.findViewById(%d);
    ", name, binder.value()));
            }
            builder.append("}
     }
    ");
            return builder.toString();
        }
        public String getProxyClassFullName() {
            // 返回代理类全限定名
            return mPackageName + "." + mProxyClassName;
        }
        public void addVariable(VariableElement element) {
            mInjectVariables.add(element);
        }
    }
    

    它的功能是为我们需要的代理类的生成源码,这里使用了字符串拼接的方式,也有许多第三方库可供使用,如Square公司的JavaPoet。

    2.4、使用处理器

    处理器编写完成后需要在主模块的build.gradle文件中进行配置才能生效,一是引入存放注解的“test-annotations”模块,二是指定“test-compiler”模块中的注解处理器。

    dependencies {
        ...
        annotationProcessor project(':test-complier')
        compile project(path: ':test-api')
    }
    

    接着在MainActivity中用 @BindView 注解一个控件然后编译项目,编译完成后处理器自动生成的类就可以在主模块的 buildgeneratedsourceaptdebug包名 下找到。

    package com.mmmmar.androidannotation;
    
    public final class MainActivity$$ViewInject {
        public MainActivity$$ViewInject(MainActivity obj) {
            obj.mTextView = obj.findViewById(2131165307);
        }
    }
    

    2.5、完成控件绑定

    生成代理类后我们可以在源码中进行手动调用,但是比较优雅的做法是像ButterKnife一样提供一个统一的调用工具,接下来我们在“test-api”模块中进行实现。

    public class ButterKnife {
    
        public static void bindView(Activity activity) {
            bindView(activity, activity.getWindow().getDecorView());
        }
        public static void bindView(Object object, View view) {
            Class clazz = object.getClass();
            String clazzName = clazz.getCanonicalName() + "$$ViewInject";
            try {
                Class proxy = clazz.getClassLoader().loadClass(clazzName);
                Constructor constructor = proxy.getConstructor(clazz);
                constructor.newInstance(object);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    因为处理器生成的代理类的逻辑都在构造函数中,所以我们只需要通过类加载器根据类名获取到代理类的Class对象,然后调用其构造函数即可完成功能。虽然这里也用到了反射,但只是用来调用一次代理类的构造函数,所以不会对性能照成影响。  

    参考: Android 如何编写基于编译时注解的项目

  • 相关阅读:
    HTML5
    9.13 开课第十天(JS脚本语音:语句:循环)
    php函数
    php基础语法
    mysql常用函数整理
    数据库经典练习题整理
    数据库练习小结
    数据库:高级查询
    CRUD操作
    SQL语句
  • 原文地址:https://www.cnblogs.com/mmmmar/p/8662084.html
Copyright © 2011-2022 走看看