Android 架构师之路 APT (1)

作者: zidea | 来源:发表于2020-05-05 20:17 被阅读0次
    Android-Developers.jpg

    今天我们先给出 demo 的代码,随后会就 APT 技术进行详细介绍

    APT

    目标通过自己视图绑定功能来了解如何 APT 帮助我们在编译期间自动生成代码。在网上收集一些资料自己实现一下。

    • 目标使用 APT 实现类似 Butter Knife 库提供的通过注解实现视图绑定的功能
      最近使用 binding 实现视图绑定功能。有点渐渐远离了 ButterKnife ,不过这个库还是给我留下美好的回忆,简洁明了的 API 让人印象深刻。

    创建一个空 Android 项目,接下我们添加模块到项目,每一个模块在 APT 中实现不同功能。

    创建模块

    • 创建 zi-annotation 注意这是一个 (java 模块),在这个模块中,可以创建注解 ZiViewBind


      apt_create_java_module.JPG
    • 创建 zi-api (android 模块)提供用户可以调用 api ,这里实现 view 的绑定。


      apt_create_android_module.JPG
    • 创建 zi-compiler(java 模块) 编写注解处理 。
    No. 模块名 模块类型 说明
    1 zi-annotation java 模块 在这个模块中,可以创建注解
    2 zi-api Android 模块 提供用户可以调用 api
    3 zi-compiler java 模块 编写注解处理

    Android Studio 在当我们创建好模块自动在 setting 文件中写入了这些模块

    rootProject.name='zi application'
    include ':app'
    include ':zi-annotation'
    include ':zi-api'
    include ':zi-compiler'
    

    处理依赖关系

    • build.gradle(zi-compiler)
    apply plugin: 'java-library'
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'com.google.auto.service:auto-service:1.0-rc2'
        implementation 'com.squareup:javapoet:1.10.0'
        implementation project(':zi-annotation')
    }
    
    sourceCompatibility = "7"
    targetCompatibility = "7"
    
    
    • 注意 zi-compiler 依赖于 zi-annotation 模块
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.squareup:javapoet:1.10.0'
    

    在主项目中添加刚刚创建好的几个模块,值得注意 zi-compiler 为 annotationProcessor

    dependencies {
        ...
    
        implementation project(':zi-annotation')
        implementation project(':zi-api')
        annotationProcessor project(':zi-compiler')
    }
    

    到此我们已经完成模块创建、添加以及他们之间依赖关系,剩下的工作就是为这些模块添加代码。


    project_architeure.JPG

    zi-annotation 模块

    创建 ZiBindView 注解

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

    @Target说明了Annotation所修饰的对象范围

    说明
    CONSTRUCTOR 用于描述构造器
    FIELD 用于描述域
    LOCAL_VARIABLE 用于描述局部变量
    METHOD 用于描述方法
    PACKAGE 用于描述包
    PARAMETER 用于描述参数
    TYPE 用于描述类、接口(包括注解类型) 或enum声明

    zi-compiler 模块

    创建 ZiBindViewProcessor 和 ClassCreatorProxy
    在代码中不涉及的注解这里就不解释了

    @Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。

    注解处理器需要继承于 AbstractProcessor ,其中部分代码写法基本是固定的。

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

    getSupportedAnnotationTypes() 返回支持的注解类型
    getSupportedSourceVersion() 返回支持的源码版本

        @Override
        public Set<String> getSupportedAnnotationTypes() {
            HashSet<String> supportTypes = new LinkedHashSet<>();
            //获取类名
            supportTypes.add(ZiBindView.class.getCanonicalName());
            return supportTypes;
        }
    

    这里,getName()返回的是虚拟机里面的class的表示,而getCanonicalName()返回的是更容易理解的表示。其实对于大部分class来说这两个方法没有什么不同的。但是对于array或内部类来说是有区别的。
    另外,类加载(虚拟机加载)的时候需要类的名字是getName。

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

    收集信息

    谓信息收集,就是根据我们声明,得到对应 Element,然后注解进行收集所需的信息,这些信息用于后期生产对象

    生产代理类(在编译时,将文本生产为类的生产代理类 ClassCreatorProxy

    针对每一个都会生产一个注解类

    初始化

    public interface ProcessingEnvironment {
        Elements getElementUtils();
        Types getTypeUtils();
        Filer getFiler();
        Locale getLocale();
        Messager getMessager();
        Map<String, String> getOptions();
        SourceVersion getSourceVersion();
        
    }
    

    ZiBindViewProcessor 完整代码

    @SupportedSourceVersion(SourceVersion.RELEASE_7)
    @SupportedAnnotationTypes({"zidea.example.com.zi_compiler.ZiBindViewProcessor"})
    @AutoService(Processor.class)
    public class ZiBindViewProcessor extends AbstractProcessor {
    
        //跟日志相关的辅助类
        private Messager mMessager;
        //跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
        private Elements mElementUtils;
        private Map<String,ClassCreatorProxy> mProxyMap = new HashMap<>();
    
        //利用 ProcessingEnvironment 对象获取 Elements
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            mMessager = processingEnvironment.getMessager();
            mElementUtils = processingEnvironment.getElementUtils();
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            HashSet<String> supportTypes = new LinkedHashSet<>();
            //获取类名
            supportTypes.add(ZiBindView.class.getCanonicalName());
            return supportTypes;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        // 收集信息
        //  所谓信息收集,就是根据我们声明,得到对应 Element,然后注解进行收集所需的信息,这些信息用于后期生产对象
        // 生产代理类(在编译时,将文本生产为类的生产代理类 ClassCreatorProxy,针对每一个都会生产一个注解类
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            //输出信息
            mMessager.printMessage(Diagnostic.Kind.NOTE,"processing...");
    
            //根据注解获取所需信息
    
            //用来获取,注解所修饰的Element对象,getElementsAnnotatedWith 用于获取注解 ZiBindView 的 Element 对象
            Set<? extends Element> elements =roundEnvironment.getElementsAnnotatedWith(ZiBindView.class);
    
            //遍历说有在类中,添加了 ZiBindView 类
            for (Element element : elements){
    
                VariableElement variableElement = (VariableElement) element;
                TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
                String fullClassName = classElement.getQualifiedName().toString();
    
                ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
                if(proxy == null){
                    proxy = new ClassCreatorProxy(mElementUtils,classElement);
                    mProxyMap.put(fullClassName,proxy);
                }
                ZiBindView bindAnnotation = variableElement.getAnnotation(ZiBindView.class);
                int id = bindAnnotation.value();
                proxy.putElement(id,variableElement);
    
            }
    
            for(String key:mProxyMap.keySet()){
                ClassCreatorProxy proxyInfo = mProxyMap.get(key);
                JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();
    
                try {
                    javaFile.writeTo(processingEnv.getFiler());
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
            mMessager.printMessage(Diagnostic.Kind.NOTE,"process finish ...");
            return true;
        }
    }
    
    
            for(String key:mProxyMap.keySet()){
                ClassCreatorProxy proxyInfo = mProxyMap.get(key);
                JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();
    
                try {
                    javaFile.writeTo(processingEnv.getFiler());
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
    
    • 返回用来创建新的类的 filer

    ClassCreatorProxy 完整代码

    public class ClassCreatorProxy {
        //绑定的类名称
        private String mBindingClassName;
        //绑定的包名称
        private String mPackageName;
        private TypeElement mTypeElement;
        private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();
    
        public ClassCreatorProxy(Elements elementUtils, TypeElement classElement){
            this.mTypeElement = classElement;
    
            PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
            String packageName = packageElement.getQualifiedName().toString();
            String className = mTypeElement.getSimpleName().toString();
            this.mPackageName = packageName;
            this.mBindingClassName = className + "_ViewBinding";
        }
    
        public void putElement(int id, VariableElement element){
            mVariableElementMap.put(id,element);
        }
    
        public String generateJavaCode(){
            StringBuilder builder = new StringBuilder();
            builder.append("package ").append(mPackageName).append(";\n\n");
            builder.append("import com.example.gavin.apt_library.*;\n");
            builder.append('\n');
            builder.append("public class ").append(mBindingClassName);
            builder.append(" {\n");
    
            generateMethods(builder);
            builder.append('\n');
            builder.append("}\n");
            return builder.toString();
        }
    
    
        private void generateMethods(StringBuilder builder) {
            builder.append("public void bind(" + mTypeElement.getQualifiedName() + " host ) {\n");
            for (int id : mVariableElementMap.keySet()) {
                VariableElement element = mVariableElementMap.get(id);
                String name = element.getSimpleName().toString();
                String type = element.asType().toString();
                builder.append("host." + name).append(" = ");
                builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));\n");
            }
            builder.append("  }\n");
        }
    
        public String getProxyClassFullName() {
            return mPackageName + "." + mBindingClassName;
        }
    
        public TypeElement getTypeElement() {
            return mTypeElement;
        }
    //JavaPoet是提供于自动生成java文件的构建工具类框架,使用该框架可以方便根据我们所注解的内容在编译时进行代码构建。
        public TypeSpec generateJavaCode2() {
            TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(generateMethods2())
                    .build();
            return bindingClass;
    
        }
    
        private MethodSpec generateMethods2() {
            ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addParameter(host, "host");
    
            for (int id : mVariableElementMap.keySet()) {
                VariableElement element = mVariableElementMap.get(id);
                String name = element.getSimpleName().toString();
                String type = element.asType().toString();
                methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
            }
            return methodBuilder.build();
        }
    
        public String getPackageName(){
            return mPackageName;
        }
    }
    

    zi-api 模块

    public class ZiBindViewTools {
    
        public static void bind(Activity activity) {
    
            Class clazz = activity.getClass();
            try {
                Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
                Method method = bindViewClass.getMethod("bind", activity.getClass());
                method.invoke(bindViewClass.newInstance(), activity);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
    Method method = bindViewClass.getMethod("bind", activity.getClass());
    method.invoke(bindViewClass.newInstance(), activity);
    

    我们通过

    ### 使用 Api 
    我们已经完成 APT 部分代码,那么如何使用我们创建好 ZiBindView 呢?具体使用方法和 butterKnife 基本一样。
    ```java
    public class MainActivity extends AppCompatActivity {
    
        @ZiBindView(value = R.id.main_activity_textView)
        TextView textView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ZiBindViewTools.bind(this);
            textView.setText("Zidea");
        }
    }
    
    Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
    

    但是当我们运行时候可能还无法生存代码,这是因为使用 gradle 版本过高,需要我们自己手动创建,

    • 首先将目录从 Android 切换到 Project 视图
    • zi-compiler目录下创建如图结构resources/META-INF/services文件目录
    • 然后在目录下添加一个问题file 即可,在 file中添加如下内容
      001.JPG
      也就是我们刚刚创建 ZiBindViewProcessor文件包名加文件名
      002.JPG

    相关文章

      网友评论

        本文标题:Android 架构师之路 APT (1)

        本文链接:https://www.haomeiwen.com/subject/cowmghtx.html