美文网首页架构师之路
2. Android 3分钟手写ButterKnife

2. Android 3分钟手写ButterKnife

作者: 鹏城十八少 | 来源:发表于2021-12-23 21:09 被阅读0次

    今天开始架构师之路!分为6节课,以手写retofit ,Butterknife,Arount,Dagger2,hilit,ASM ,AOP为主

    ****APT :处理提取和处理 Annotation 的代码统称为(Annotation Processing Tool)

    作用

    使用APT的优点就是方便、简单,可以少些很多重复的代码。

    用过ButterKnifeDaggerEventBus等注解框架的同学就能感受到,利用这些框架可以少些很多代码,只要写一些注解就可以了。

    编写注解处理器

       和运行时注解的解析不一样,编译时注解的解析需要我们自己去实现一个注解处理器。
    

    注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。而且这些生成的Java文件同咱们手动编写的Java源代码一样可以调用。(注意:不能修改已经存在的java文件代码)。

       注解处理器所做的工作,就是在代码编译的过程中,找到我们指定的注解。然后让我们更加自己特定的逻辑做出相应的处理(通常是生成JAVA文件)。
    
       注解处理器的写法有固定套路的,两步:
    

    注册注解处理器(这个注解器就是我们第二步自定义的类)。

    自定义注解处理器类继承AbstractProcessor。

    如何看:java里面的jdk。会在resources的里面有META-INF。里面会有注解处理器的文件!javax.annotation.

    所以apt用的是java的lib,不是Android !

    注解处理器会在路径Build/classes/java/main/com/META-INF/Services/javax_anontation_process.

    APT本质:生成java类

    APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。

    简单来说就是在编译期,通过注解生成.java文件。

    详细解释: 它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),

    APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

    在这里我们将在每一个业务组件的 build.gradle 都引入ActivityRouter 的 Annotation处理器,我们将会在声明组件和Url的时候使用,annotationProcessor是Android官方提供的Annotation处理器插件,代码如下:

    dependencies {

    compile fileTree(dir: 'libs', include: ['*.jar'])
    
    annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
    

    }

    [图片上传失败...(image-c4ef24-1640265031541)]

    JavaPoet

    更好的方案:通过javapoet可以更加简单得生成这样的Java代码。(后面会说到)

    JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件

    需要添加JavaPoet的依赖

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: Consolas; font-size: 0.817rem;">implementation'com.squareup:javapoet:1.9.0'</pre>

    javapoet的详细内容:

    1.方法名

    2.返回值

    3.打印

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: Consolas; font-size: 0.817rem;">MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("T.out.println(S)", System.class, "Hello, JavaPoet!")
    .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build(); javaFile.writeTo(System.out);</pre>

    生成的代码

    上面的功能一直在完成一件事情,那就是生成Java代码,那么生成的代码在哪?
    app/build/generated/source/apt中可以找到生成的Java文件

    比如:MainActivity$$ARounter

    autoService:主要作用:注册,****相当于安卓的四大组件需要注册一样!

    google auto 中的autoservice 可以帮助我们生成对应的配置:使用AutoService注解,可以自动生成meta信息

    spi 是一种服务发现的标准,对于开发中我们通常需要编写 META-INF/services 文件夹中定义的类。

    自定义注解处理器注册才能被Java虚拟机调用,在上面的博客第四小节中用的方法是手动注册,这比较违反程序员懒的特点,在里面也提到了自动注册的方法,就是AutoService

    介绍下依赖库auto-service
    在使用注解处理器需要先声明,步骤:
    1、需要在 processors 库的 main 目录下新建 resources 资源文件夹;
    2、在 resources文件夹下建立 META-INF/services 目录文件夹;
    3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
    4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;)
    这样声明下来也太麻烦了?这就是用引入auto-service的原因。
    通过auto-service中的@AutoService可以自动生成AutoService注解处理器是Google开发的,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的

    怎么让jvm在编译期间调用我们自己写的这个注解处理器呢?

    有一个快捷办法就是使用谷歌的开源库auto,然后使用它提供的AutoService注解来实现,

    另外一种办法就是自己手动去创建指定的文件夹,然后配置我们的注解处理器的路径。

    注解处理器的debug调试

    注解处理器的debug 跟普通的代码debug 有点不同:

    在当前工程路径下输入命令

    gradlew --no-daemon

    -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac

    并在Edit Configurations 中新添加一个远程配置(remote),名字随意,端口为

    5005。然后点击debug 按钮,就可以连接上远程调试器进行Annotation 的调试了。

    demo地址:

    https://github.com/jaminchanks/AnnotationProcess-Demo

    参考博客:

    https://www.jianshu.com/p/7af58e8e3e18

    https://www.jianshu.com/p/6955a56844d7

    点击世界是否会用到动态代理???---另外一种方案!

    https://blog.csdn.net/u010008118/article/details/100896148

    ButterKnife:黄油刀,被废弃了。现在已经被viewBind和databing取代,依赖注入框架。但是原理很重要!

    问题:他和Arounter结合的时候会又什么问题?

    R2?为什么会产生这样的问题?

    编****译时注解实战: 手写 ButterKnife

    4个模块

    1.APP :使用

    2.annoations:用于定义接口

    3.annotation_complier:注解处理器,用于自动生成文件,注解处理器要放在java library里面

    annotation_complier继承:abstractProcessor

    4. lib :通过反射调用

    重写几个方法

    然后我们还需要重写AbstractProcessor

    getSupportedAnnotationTypes() 方法

    getSupportedSourceVersion() 方法

    getSupportedAnnotationTypes() 方法用来指定该注解处理器是用来处理哪个注解的

    getSupportedSourceVersion()方法用来指定java版本,一般给值为SourceVersion.latestSupported()

    注意:如果必要的方法没有写,会导致不执行主要的处理方法

    里面重写的方法可以用注解代替, evenbus源码就是这么写的

    那我们开始:创建项目,4步搞定

    创建Android Module命名为app
    创建Java library Module命名为 apt-annotation
    创建Java library Module命名为 apt-processor 依赖 apt-annotation
    创建Android library Module 命名为apt-library依赖 apt-annotation、auto-service

    分析:

    1.看下使用流程:1.有一个注解 2.有自动生成的类可以自动注入!

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;"> @BindView(R.id.tv_bangding)
    TextView btn; @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this); //自动生成的类有一个bind方法,进行动态注入
    }
    }</pre>

    手写步骤:

    1.新建java module。把注解方进来。

    为什么要把注解单独放一个module?

    因为主module和编译module都需要用!

    那为什么不把注解放在自动生成注解的module里面?

    field注解+编译时+java module

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;">@Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindView {
    int value(); }
    </pre>

    apt-annotation(自定义注解)

    创建注解类BindView

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

    @Retention(RetentionPolicy.CLASS):表示编译时注解
    @Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)

    @Retention: 定义被保留的时间长短
    RetentionPoicy.SOURCE、RetentionPoicy.CLASS、RetentionPoicy.RUNTIME
    @Target: 定义所修饰的对象范围
    TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE等

    这里定义了运行时注解BindView,其中value()用于获取对应Viewid

    2.新建java module,把注解处理器需要的写进来,依赖javapoat和autoservice

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;">implementation 'com.google.auto.service:auto-service:1.0-rc2' implementation 'com.squareup:javapoet:1.10.0' implementation project(':apt-annotation')</pre>

    需要先模拟好生成的类,在主程序的build/generated/source/apt

    [图片上传失败...(image-fd1f62-1640265031545)]

    在自动生成模块里面会生成注册器

    [图片上传失败...(image-15d481-1640265031545)]

    一共5个方法:

    其他3个方法可以用注解的形式下。

    然后init方法和process方法注解处理器解析:

    1.得到所以有标记的注解元素或者节点

    2.得到包名

    3.得到类名

    4.拼接类

    5.生成java文件

    主要的一些对象:

    1.元素

    2.文件创造器

    3.日志信息打印输出messager

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;">@AutoService(Processor.class)
    public class BindViewProcessor extends AbstractProcessor {

    private Messager mMessager; // 打印
    

    private Elements mElementUtils;
    private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>(); /***

    • 初始化一些类:比如日志,读写流 * @param processingEnv
      */
      @Override
      public synchronized void init(ProcessingEnvironment processingEnv) {
      super.init(processingEnv);
      mMessager = processingEnv.getMessager();
      mElementUtils = processingEnv.getElementUtils();
      }

      /***

    • 需要处理注解的类型 * @return
      */
      @Override
      public Set<String> getSupportedAnnotationTypes() {
      HashSet<String> supportTypes = new LinkedHashSet<>();
      supportTypes.add(BindView.class.getCanonicalName());
      return supportTypes;
      }

      /***

    • java的版本 * @return
      */
      @Override
      public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
      }

      @Override
      public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
      mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
      mProxyMap.clear();
      //得到所有的带指定的注解BindView
      Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
      for (Element element : elements) {
      VariableElement variableElement = (VariableElement) element;
      TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
      String fullClassName = classElement.getQualifiedName().toString();
      //elements的信息保存到mProxyMap中,key:类全名
      ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
      if (proxy == null) {
      proxy = new ClassCreatorProxy(mElementUtils, classElement);
      mProxyMap.put(fullClassName, proxy);
      }
      BindView bindAnnotation = variableElement.getAnnotation(BindView.class);//通过元素得到注解类
      int id = bindAnnotation.value();//得到注解类里面的值
      proxy.putElement(id, variableElement);
      }
      //拿到开始map存放的信息
      //通过javapoet生成java类 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; }

    }</pre>

    <pre style="margin: 8px 0px;">

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">/**

    • 加入Method * javapoet */ 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); //VariableElement就是相应的元素
      String name = element.getSimpleName().toString(); //控件名字
      String type = element.asType().toString();//控件的类型,是btn还是text
      methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
      }
      return methodBuilder.build(); }</pre>

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">最重要的是要得到VariableElement</pre>

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">主要就是从ElementsTypeElement得到想要的一些信息,如package name、Activity名、变量类型、id等,</pre>

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace;">使用的时候:
    public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView mTextView;
    

    @BindView(R.id.btn)
    Button mButton;</pre>

    </pre>

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">获取和解析控件:</pre>

    <pre style="margin: 8px 0px;">

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">VariableElement element = mVariableElementMap.get(id); //VariableElement就是相应的元素 String name = element.getSimpleName().toString(); //控件名字 String type = element.asType().toString();//控件的类型,是btn还是text</pre>

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43);">常用的几个类(打印,元素,javapoat文件读写操作JavaFile)</pre>

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43);"> <pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">public class BindViewProcessor extends AbstractProcessor {

    private Messager mMessager; // 打印
    

    private Elements mElementUtils; // 对应的元素
    private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>(); //把带注解的元素封装在map中</pre>

    3、封装一个调用module。 </pre>

    <pre style="margin: 8px 0px;"> #### apt-library 工具类在BindViewProcessor中创建了对应的xxxActivity_ViewBinding.java,我们改怎么调用?当然是反射啦!!!apt-library 工具类

    为什么用通过反射调用?

    因为这些类都是动态生成的,根据不同的activity动态生成的!

    </pre>

    <pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">public class BindViewTools {

    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();
    }
    }
    }
    </pre>

    <pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">4.使用编译时注解</pre>

    <pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">

    <pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace;">public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView mTextView;
    

    @BindView(R.id.btn)
    Button mButton; @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    BindViewTools.bind(this);
    mTextView.setText("bind TextView success");
    mButton.setText("bind Button success");
    }
    }</pre>

    </pre>

    </pre>

    </pre>

    相关文章

      网友评论

        本文标题:2. Android 3分钟手写ButterKnife

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