注解

我所知道的偷懒的方式

2019-11-19  本文已影响0人  ___刘辉良

前言

在现今的软件开发过程中,软件开发人员将更多的精力投入在了重复的相似劳动中。特别是在如今特别流行的 MVC 架构模式中,软件各个层次的功能更加独立,同时代码的相似度也更加高。所以我们需要寻找一种来减少软件开发人员重复劳动的方法,让程序员将更多的精力放在业务逻辑以及其他更加具有创造力的工作上。

服务端的技术发展了很多年,有很多值得可以借鉴的地方。有的时候,在跟后台沟通的时候,发现他们在设计好数据库表结构的时候,经常可以一键生成常见的功能(增、删、改、查)。后来知道,因为相识程度非常的高。所以他们经常以代码来生成代码。就这样完成了一站式的功能。

更多时候,我在考虑,为何他们不优先考虑封装呢?

  1. 可能会经常发生变动。
  2. 人员因素吧

Android 的一种"偷懒"

Android 中有种模板编程的偷懒方式。但不是今天的主角。今天的主角是Annotation Processor(注解处理器)。 在最近查阅源码的时候。发现很多框架都会使用到Annotation Processor比如说,ButterKnifeDaggerARouter等等。所以理解Annotation Processor(注解处理器)的原理,是一个Android程序员必须具备的技能。

1. Annotation Processor

Annotation Processor直译成中文就是(注解处理器),就是能够对注解进行处理。既然能够对注解进行处理,那么先定义一个简单的注解。

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

2. 使用注解

定义完注解后,我们会想着在什么时候使用它呢? 还记得上述的核心是为了偷懒,那么
Android存在有重复性非常高而且难度极地的代码,就是对控件的获取和处理。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView1=(TextView) findViewById(R.id.tvHello);
        textView1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            }
        });
    }
}

这样的模板代码不仅很浪费时间,而且代码的美观程度也大大的降低了。那么我们现在尝试着用自己定义的注解去处理。

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tvHello)
    TextView tvHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @OnClick(R.id.tvHello)
    public void changeText2() {
        Toast.makeText(this, "2222", Toast.LENGTH_LONG).show();
    }
}

上述的代码,就会比较清晰。不会有获取控件的过程的代码。声明即使用了。那么仅仅这样打上一个注解,能够自动的获取控件吗?答案是不可能的。我们还缺少一步编写注解处理器(其实就是编写遇到这个注解改怎么处理)。

3. 编写注解处理器
xx01 最终生成的类

在编写注解处理器之前,先尝试着去编写一个类。这个类的职责就是获取控件。

public final class MainActivity$ViewBinding {
  public MainActivity$ViewBinding(MainActivity target, View source) {
    if(target == null)  return;
    if(source == null)  return;
    target.tvHello = (android.widget.TextView)source.findViewById(2131165275);
  }

  public MainActivity$ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }
}

上面的这个类,就是需要注解处理器帮我们自动生成的模板代码。我们必须知道自己需要什么代码,才能控制注解处理器生成我们想要的代码。那么观察一下最终生成的代码,其中有几个需要注意的:

  1. 需要遍历所有需要处理的注解。在本文中我们只关心BindView
  2. 当我们遍历出所有带有BindView的注解的时候,我们需要为这个字段赋值。而赋值的操作,我们是通过属性直接赋值(默认的访问权限是包访问权限)

target.tvHello = (android.widget.TextView)source.findViewById(2131165275);

所以这个由注解处理器帮我生成的类,需要跟目标类在同级包下。

  1. 这个类的名字是按照一定格式生成的,在本文中它是由目标类+$ViewBinding。

到这里,需要注意的点都讲完了,接下开始来编写注解处理器的核心代码。

xx02 项目目录的划分

在知道了我们最终需要生成什么代码之后,就需要对整个目录进行规划一下,因为这个工具不仅仅是当前这个项目会被使用。很有很多项目将对其引用。你可能会将其发布到JCenter上。

image.png

根据上面的图,我们分别建立3module。其中2个是java libaryandroid libary

image.png

最终的目录结构是这样的

image.png

然后我们将刚才编写的BindView注解放入inject-annotation模块中

xx03 编写注解处理器

在划分话目录结构后,我们可以编写注解处理器的核心代码了,也就意味着,我们需要把它的代码放置在inject-compiler中。

需要在inject-compiler下的build.gradle导入几个库

    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
    api 'com.squareup:javapoet:1.10.0'
    implementation project(':inject-annotation')

介绍一下这几个库,

  1. auto-service 是帮助生成META-INF/services/javax.annotation.processing文件中的内容
  2. javapoet 更加面向对象的输出代码
  3. inject-annotation 需要处理的注解。

在添加完这些东西之后,我们就需要创建一个注解处理器了

  @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        HashMap<TypeElement, List<Element>> datas = new HashMap<>();
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            TypeElement originalType = (TypeElement) element.getEnclosingElement();
            if (datas.get(originalType) == null) {
                List<Element> elements = new ArrayList<>();
                datas.put(originalType, elements);
            }
            List<Element> dd = datas.get(originalType);
            dd.add(element);
        }

        for (Map.Entry<TypeElement, List<Element>> entry : datas.entrySet()) {
            TypeElement key = entry.getKey();
            List<Element> value = entry.getValue();
            createFile(key, value);
        }
        return false;
    }

核心代码大概是上面的,就是遍历类中的注解元素,包装成一个Map数据的数据结构。

image.png

然后将构建后的数据结构,进行处理,按照之间MainActivity$ViewBinding的所预想的规则进行输出。

  private MethodSpec createCustomerConstructor2View(TypeElement originalType, List<Element> elements) {
        ParameterSpec targetParamSpec = ParameterSpec.builder(TypeName.get(originalType.asType()), "target").build();
        MethodSpec.Builder constructor1 = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(targetParamSpec)
                .addParameter(mViewParameterSpec)
                .addStatement("if(target == null)  return")
                .addStatement("if(source == null)  return");

        for (Element element : elements) {
            String variateName = element.getSimpleName().toString();
            String variateType = element.asType().toString();
            int resId = element.getAnnotation(BindView.class).value();
            constructor1.addStatement("target.$L = ($N)source.findViewById($L)", variateName, variateType, resId);
        }
        return constructor1.build();
    }

这样我们就完成了注解处理器的编写,当你编写后重新Rebuild Project后,如果不出意外的话,你可以找到下面这个文件。

image.png
xx03 调试Annotation Processor(注解处理器)

如果你在编写注解处理器可能不是你预想,那么断点调试就变得非常的重要了。那么接下来介绍如何调试注解处理器.

  1. 打开Edit Configurations新建一个Remote Configurations
image.png
  1. Terminal中运行下面命令

./gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac

image.png
  1. 需要断点的地方,下断点。

  2. Debug 刚才添加的Configurations

image.png

5.它就可以正常的断点了

image.png
xx04 调用生成的代码

当我们完成了注解处理器的编写,也正常的生成了MainActivity$ViewBinding文件后,我们就需要调用所生成的类,让它来帮我们完成控件的注入。因为其中会涉及到对Activity的引用,所以这个包是android libary。也就意味着,接下来的代码需要在inject-core模块中编写。

我们在进入一个Activity或者Fragment的时候,能够调用inject方法,而inject的实现如下:

public static void inject(Activity activity) {
        String name = activity.getClass().getSimpleName();
        String packName = activity.getPackageName();
        String fullName = name + "$ViewBinding";
        try {
            Class targetClass = activity.getClassLoader().loadClass(packName + "." + fullName);
            Constructor constructor = targetClass.getConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

就是通过反射,然后实例化刚才的对象。这样的话,我们就完成了控件的注入了。

4. 发布到JCenter

因为我们项目中存在有jaraar格式的包,所以需要分别上传。在编写上传任务之前,需要先在最外层的build.gradle添加plugin,请注意插件的版本。不然会出现一些奇奇怪怪的问题。

buildscript {
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0'
        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6'

    }
}

allprojects {
    repositories {
        google()
        jcenter()
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

  1. 编写jar格式的gradle,在最外层新建jar_publish.gradle文件,然后在:inject-compilerbuild.gradle中 apply。
image.png
  1. 编写aar格式的gradle,在最外层新建aar_publish.gradle文件,然后在:inject-corebuild.gradle中 apply。
image.png

然后依次上传即可。

本文中的代码

  1. https://github.com/BelongsH/APTDemo

相关链接

  1. https://github.com/google/auto/tree/master/service
上一篇下一篇

猜你喜欢

热点阅读