ButterKnife 是如何通过 Gradle Plugin 来生成 R2 类的原理分析

一、学习目标

本节的学习目标是分析 butterknife 利用 Gradle Plugin 生成 R2.java 文件的过程。

  • 新建工程,引入 butterknife ,并在 library 中使用 butterknife。

  • 分析为何需要生成 R2.java 文件。

  • R2.java 文件的生成。

  • 分析 R.java 和 R2.java 的区别。

  • library 工程出现资源 id 冲突。

二、在 library 使用 butterknife

  • 新建一个 AS 工程,并创建一个 Library 类型的 module。

《ButterKnife 是如何通过 Gradle Plugin 来生成 R2 类的原理分析》 新建工程

  • 依赖 butterknife

在工程根目录下/build.gradle

dependencies {
    classpath 'com.android.tools.build:gradle:3.0.0'
    //引入 butterknife 插件
    classpath 'com.jakewharton:butterknife-gradle-plugin:8.2.0'
}

在 library/build.gradle

implementation 'com.jakewharton:butterknife:8.2.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.2.0'
  • 在 library 引入插件
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
  • 在 library module 使用 butterknife
public class MainActivity extends AppCompatActivity {

    @BindView(R2.id.textview)
    TextView textview;

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

好了,经过上面的几个步骤,我们就就可以将 butterknife 引入到我们的 library module 中是用了,根据官方文档的指示,我们在使用 @BindView(R2.id.textview) 中时,需要指定 R2 而不是 R 哦。

三、为何需要 R2.java 文件

当我们在 library 工程中这样使用 BindView(R.id.textview) 是会报错的,提示如下图所示,表示注解中的值需要接收的是一个常量

《ButterKnife 是如何通过 Gradle Plugin 来生成 R2 类的原理分析》 library 中是用 R.id.xxx

我们来看看在 library 工程中生成的 R.java 文件,可以看出,R.java 类内部都是变量,而不像是 application 工程生成的 R.java 类的变量会被 final 修饰。现在我们就明白了为什么在 library 中这样使用 @BindView(R.id.textview) 会报错。

《ButterKnife 是如何通过 Gradle Plugin 来生成 R2 类的原理分析》 R 文件

下面是 Android 官方对这个问题的解释,有兴趣的可以点击进去看看。

Non-constant Fields in Case Labels

早期的 butterknife 版本是不支持在 library 使用的,不过现在都是在搞组件化了,所以在 butterknife:8.2.0 版本之后开始支持,本文使用的源代码也是基于 8.2.0 版本。

实现的原理就是将 R.java 文件拷贝一份到 R2.java 中,并且将每一个属性变成常量,这样最终就变成 @BindView(R2.id.textview) 了

四、R2.java 文件的生成

我们在前面为我们的工程中引入了 butterknife-gradle-plugin:8.2.0 插件,这个东西就是用来负责生成 R2.java 的。

接下来,我们进入源代码分析一下,它内部是如何去生成的:

4.1 ButterKnifePlugin 源码

ButterKnifePlugin 中主要做了以下几件事:

  • 1、判断该 plugin 只能用于 android 工程。

  • 2、在 processResources task 中获取需要生成 R2 文件所在的目录和 R 文件。

  • 4、调用 FinalRClassBuilder.brewJava 生成 R2 文件。

public class ButterKnifePlugin implements Plugin<Project> {

  @Override
  void apply(Project project) {
  
    //1.判断该plugin 只能用于 android 工程
    if (!(project.plugins.hasPlugin(LibraryPlugin) || project.plugins.hasPlugin(AppPlugin))) {
      throw new IllegalStateException('Butterknife plugin can only be applied to android projects')
    }
    
    def variants
    if (project.plugins.hasPlugin(LibraryPlugin)) {
      variants = project.android.libraryVariants
    } else {
      variants = project.android.applicationVariants
    }
    //核心代码
    project.afterEvaluate {
      variants.all { BaseVariant variant ->
        variant.outputs.each { BaseVariantOutput output ->
        
          //处理 processResources 这个 task 
          //例如 :library:process${FlavorName}ReleaseResources
          output.processResources.doLast {
            //2.确定需要生成 R2.java 文件所在的目录和R.java 文件。
            //packageForR就是表示当前的包名
            //将 R.java 所在的包中的.替换为/,相当于 com.example.xxx —> com/example/xxx
            File rDir = new File(sourceOutputDir, packageForR.replaceAll('\\.',
                    StringEscapeUtils.escapeJava(File.separator)))
            File R = new File(rDir, 'R.java')
            //3.生成 R2.java 文件
            FinalRClassBuilder.brewJava(R, sourceOutputDir, packageForR, 'R2')
          }
        }
      }
    }
  }
}

4.2、生成 R2 文件

下面摘取了核心的代码,下面这段代码主要是将对应的 R 文件的变量写入到 R2 文件中,并使用 public static final 标识为常量。

private static void addResourceField(TypeSpec.Builder resourceType, VariableDeclarator variable,
    ClassName annotation) {
  String fieldName = variable.getId().getName();
  String fieldValue = variable.getInit().toString();
  FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(int.class, fieldName)
      .addModifiers(PUBLIC, STATIC, FINAL)
      .initializer(fieldValue);
  if (annotation != null) {
    fieldSpecBuilder.addAnnotation(annotation);
  }
  resourceType.addField(fieldSpecBuilder.build());
}

最终生成的 R2 文件如下:

《ButterKnife 是如何通过 Gradle Plugin 来生成 R2 类的原理分析》 R2文件

现在 R2 文件的属性都是常量了,其实也就是欺骗编译器吧。

@BindView(R2.id.textview)
TextView textview;

4.3 R 和 R2 的区别?

  • 1、存放在 build/generated/source/r/debug/包名/ 目录下。

  • 2、R2文件的属性变成了 public static final 常量,并且是使用对应的@XxRes 注解标识

  • 3、两个类的属性资源 id 都是一样的。

//R 文件
public static final class id {
      public static int textview = 0x7f0c0086;
}
//R2文件
public static final class id {
      @IdRes
      public static final int textview = 0x7f0c0086;
}

4.4 library 工程出现资源 id 冲突

在多 library 库合并时,有可能出现资源 id 冲突的现象,那么这时 R2 类每一个属性的资源 id 还是旧 R 类的资源 id,这样会不会有问题呢?

我们来看看下面的代码,这是 butterknife 通过 APT 帮我们自动生成的一个类 MainActivity_ViewBindingMainActivity_ViewBinding构造是在 Butterknife.bind(this)时调用。这里会给 MainActivitytextview 属性赋值。在这里已经指定了 idR.id.textview ,因此即使资源 id 冲突,那也是在合并编译期间的事了。
在运行期间还是通过 R 类引用的。R2 类只是做一个标识,告诉 APT 怎么去生成这个对应的 java 代码 ,看 findOptionalView 方法,实际最终还是 findViewById(R.id.textview) 而不是 findViewById(R2.id.textview)

public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
  //MainActivity
  protected T target;

  public MainActivity_ViewBinding(T target, Finder finder, Object source) {
    this.target = target;
    //这里会给 MainActivity 的 textview 属性赋值。
    //在这里已经指定了 id 为 R.id.textview ,因此即使资源id 冲突,这里还是通过 R 文件引用的。
    //R2类只是做一个标识,标识怎么去查找这个 id ,但是实际还是 findViewById(R.id.textview) 而不是 findViewById(R2.id.textview)。
    target.textview = finder.findRequiredViewAsType(source, R.id.textview, "field 'textview'", TextView.class);
  }
  ...
}

//Finder
public enum Finder {
    ACTIVITY {
      @Override public View findOptionalView(Object source, @IdRes int id) {
        return ((Activity) source).findViewById(id);
      }
      @Override public Context getContext(Object source) {
        return (Activity) source;
      }
    }
 }

五、总结

本文简单地分析了 ButterKnife 是如何通过 Gradle Plugin 来生成 R2.java 类,文章所描述的内容只是本人学习的总结,有错误的地方望请指正。

记录于 2019年2月21日

    原文作者:未见哥哥
    原文地址: https://www.jianshu.com/p/33bf0c0e6453
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞