一、概述
这篇文章是为了了解viewBinding是如何实现的,我很好奇为什么我们在项目的module的build.gradle中添加了如下代码
viewBinding{
enable true
}
在make项目后就能自动生成ActivityMainBinding这些类的呢?
而我们为什么通过binding就能找到对应的控件的呢?
网上的博客中大部分都是只解释了,自动生成的ActivityMainBinding类中替我们做了findViewById所以我们可以通过binding找的对应的控件,但是这个类是怎么生成的呢?
了解过JavaPoet的同学可能知道,通过javaPoet可以用代码生成类,学过注解的同学可能会想通过注解+JavaPoet就能实现这个功能了,但是viewBinding并没有使用注解啊。
通过学习我了解到了,viewBinding的实现流程如下
由图可知,viewBinding是在make项目后才执行生成文件的代码,再加上build.gradle中的enable配置,所以联想到了gradle插件。
总结:所以整体流程就是由gradle插件实现通过viewBinding的enable配置决定是否启用viewBinding的代码生成,启用的话则使用JavaPoet来实现自动生成内部实现了findViewById和控件赋值的Java类。
二、实现viewBinding
1.使用JavaPoet生成Java文件
我们生成的Java类全貌如下:
public class ActivityMainBinding {
public final ConstraintLayout rootView;
public final TextView text;
private ActivityMainBinding(ConstraintLayout rootView, TextView text) {
this.rootView = rootView;
this.text = text;
}
public ConstraintLayout getRoot() {
return rootView;
}
public static ActivityMainBinding setContentView(Activity activity, int layoutId) {
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = decorView.findViewById(android.R.id.content);
ConstraintLayout root = (ConstraintLayout) LayoutInflater.from(activity).inflate(layoutId, contentView, false);
activity.setContentView(root);
return bind(root);
}
public static ActivityMainBinding bind(ConstraintLayout rootView) {
String missingId;
TextView text = rootView.findViewById(R.id.text);
if(text == null) {
throw new NullPointerException("Missing required view with ID:"+text);
}
return new ActivityMainBinding(rootView,text);
}
}
我个人使用JavaPoet生成Java类的心得就是你先写好需要的Java类,然后根据这个模版再去使用JavaPoet生成,避免你写到一半忘记自己需要什么方法,以及参数,返回值类型的问题。
(1) 遍历module下的res/layout目录,找到所有的xml文件,并且通过xml解析获取xml中定义了id的控件id和类型;
示例:
//拼接layout目录path
String layoutPath = project.getProjectDir().getAbsolutePath()+
File.separator + "src" + File.separator + "main" +
File.separator +"res" + File.separator +"layout";
File layoutDirFile = new File(layoutPath);
System.out.println("layoutPath: "+layoutPath);
if (layoutDirFile.exists()) {//layout目录存在
File[] listFiles = layoutDirFile.listFiles();
if (listFiles != null && listFiles.length > 0) {//layout目录下有文件
for (File file : listFiles) {//遍历目录下的文件
if (file.isFile() && file.getName().endsWith(".xml")) {//找到xml格式的文件
// 是文件且是xml文件
String fileName = file.getName().replace(".xml", "");
String bindingClassName;
if (fileName.contains("_")) {
// 按下划线切割
StringBuilder tempClassName = new StringBuilder();
for (String s : fileName.split("_")) {
tempClassName.append(s.substring(0, 1).toUpperCase().concat(s.substring(1)));
}
bindingClassName = tempClassName.append("Binding").toString();
} else {
// 不包含 首字母大写
bindingClassName = fileName.substring(0, 1).toUpperCase().concat(fileName.substring(1)).concat("Binding");
}
//当前文件的绝对路径
String filePath = file.getAbsolutePath();
//通过xml解析获得xml的根标签类型
String rootEleName = XmlUtil.getRootElement(filePath).getName();
boolean isRootElementEnableViewBinding = XmlUtil.getRootElementViewBindingEnable(filePath);
if (!isRootElementEnableViewBinding){
System.out.printf("%s`s layout rootElement-%s contains view_binding attribute equals false \n", bindingClassName , rootEleName);
continue;
}
//收集xml中所有设置了id属性的控件id
List<String> ids = new ArrayList<>();
XmlUtil.getAllIdElement(XmlUtil.getRootElement(filePath), ids);
// 收集到了ID,存储起来
componentIds.put(bindingClassName.concat(JOINT_MARK1).concat(rootEleName).concat(JOINT_MARK2).concat(fileName), ids);
}
}
(2) 根据xml文件名称创建Java类的类名,根据获取到的id集合定义全局控件变量;
示例:
//创建类
TypeSpec.Builder viewBindingClassBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC);
//创建全局对象
viewBindingClassBuilder.addFields(getFieldSpecList(rootElementName, componentsAndIds));
private static List<FieldSpec> getFieldSpecList(String rootComponentName , List<String> componentsAndIds){
List<FieldSpec> fieldSpecList = new ArrayList<>();
//创建rootView变量
FieldSpec rootFieldSpec = FieldSpec.builder(getViewClassName(rootComponentName),
"rootView")
.addModifiers(Modifier.PUBLIC , Modifier.FINAL)
.build();
fieldSpecList.add(rootFieldSpec);
//遍历控件集合,根据id和控件类型创建对应类型的控件变量
for (String componentNameAndId : componentsAndIds) {
String componentId = componentNameAndId.substring(0, componentNameAndId.lastIndexOf("_"));
String componentName = componentNameAndId.substring(componentNameAndId.lastIndexOf("_") + 1);
String finalComponentName;
if (componentId.contains("_")) {
// 按下划线切割
StringBuilder tempComponentName = new StringBuilder();
String[] componentLowercaseArray = componentId.toLowerCase().split("_");
for (int i = 0; i < componentLowercaseArray.length; i++) {
if (i==0){
tempComponentName.append(componentLowercaseArray[0]);
}else{
tempComponentName.append(componentLowercaseArray[i].substring(0, 1).toUpperCase().concat(componentLowercaseArray[i].substring(1)));
}
}
finalComponentName = tempComponentName.toString();
} else {
// 不包含 首字母大写
finalComponentName = componentId.substring(0, 1).toLowerCase().concat(componentId.substring(1));
}
FieldSpec componentField = FieldSpec.builder(getViewClassName(componentName),
finalComponentName)
.addModifiers(Modifier.PUBLIC , Modifier.FINAL)
.build();
fieldSpecList.add(componentField);
}
return fieldSpecList;
}
(3) 创建构造方法,给定义的控件赋值;
// 创建构造方法
MethodSpec construct = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addParameters(getConstructorParameters(rootElementName , componentsAndIds))
.addCode(getConstructorStatements(componentsAndIds))
.build();
//向类中添加方法
viewBindingClassBuilder.addMethod(construct);
(4) 创建setContentView方法,找到decorView将布局设置给activity;
//创建setContentView方法
ClassName activityMainBindingClass = ClassName.get(packageName, className);
//创建参数
ParameterSpec activityParam = ParameterSpec.builder(ClassName.get("android.app", "Activity"),
"activity").build();
ParameterSpec layoutIdParam = ParameterSpec.builder(int.class, "layoutId").build();
MethodSpec setContentView = MethodSpec.methodBuilder("setContentView")
.addModifiers(Modifier.STATIC, Modifier.PUBLIC)
.returns(activityMainBindingClass)//添加返回类型
.addParameter(activityParam)//添加方法参数
.addParameter(layoutIdParam)
.addStatement("$T decorView = activity.getWindow().getDecorView()",
ClassName.get("android.view","View"))//添加方法体
.addStatement("$T contentView = decorView.findViewById(android.R.id.content)",
ClassName.get("android.view", "ViewGroup"))
.addStatement("$T root = ($T) $T.from(activity).inflate(layoutId, contentView, false)",
constraintLayoutClass,
constraintLayoutClass,
ClassName.get("android.view", "LayoutInflater"))
.addStatement("activity.setContentView(root)")
.addStatement("return bind(root)")
.build();
viewBindingClassBuilder.addMethod(setContentView);
(5) 创建getRoot方法,方便获取layout的根布局;
// 创建getRoot方法
ClassName constraintLayoutClass = getViewClassName(rootElementName);
MethodSpec getRoot = MethodSpec.methodBuilder("getRoot")
.addModifiers(Modifier.PUBLIC)
.returns(constraintLayoutClass)
.addStatement("return rootView")
.build();
viewBindingClassBuilder.addMethod(getRoot);
(6) 创建bind方法,通过findViewById找到控件后调用构造方法赋值。
//创建bind方法
ParameterSpec rootViewParam = ParameterSpec.builder(constraintLayoutClass, "rootView").build();
MethodSpec bindMethod = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.STATIC, Modifier.PUBLIC)
.returns(activityMainBindingClass)
.addParameter(rootViewParam)
.addStatement("$T missingId", String.class)
.addCode(getBindCode(className, componentsAndIds))
.build();
viewBindingClassBuilder.addMethod(bindMethod);
(7) 生成文件,在指定目录下生成真正的Java类文件
示例:
//创建类
TypeSpec viewBindingClass = viewBindingClassBuilder.build();
//生成文件
JavaFile javaFile = JavaFile.builder(packageName, viewBindingClass)
.build();
String filePath = project.getProjectDir().getAbsolutePath()+"/build/generated/ap_generated_sources/debug/out/viewBinding";
try {
File filer = new File(filePath);
javaFile.writeTo(filer);
System.out.println("generated successfully");
} catch (IOException e) {
System.out.println("generated unsuccessfully");
e.printStackTrace();
}
2.自定义Gradle插件
1.自定义gradle插件方式介绍
自定义gradle插件可以在以下三个地方创建,分别是:
1.构建脚本内;
2.buildSrc模块内;
3.单独项目
1.构建脚本内建方式
在build.gradle内直接创建Gradle插件
优点:
build.gradle中创建的插件将被自动编译并包含在 classpath 中,使用时无需在构建脚本内指定 classpath
缺点:
- 此插件仅在当前构建脚本中有效,对外部文件不可见,无法在当前构建脚本以外的其他地方复用此插件
2.buildSrc模块方式
rootProject/buildSrc 文件夹是 Gradle 的预留目录,用来存放当前项目私有 Gradle 插件的源代码与构建脚本
优点:
- 项目构建时,Gradle 会自动编译项目目录下的 buildSrc 文件夹下的构建脚本和源码,并将其添加到项目构建脚本的 classpath 中,因此在使用 buildSrc 中创建的插件时,无需再手动指定 classpath
- buildSrc 文件夹中构建脚本和 Gradle 插件同一项目均可见,因此同一项目中的其他模块也可以使用 buildSrc 中创建的插件
缺点:
- 此处创建的插件对外部项目不可见,无法在其他项目中复用
3.单独项目方式
采用 buildSrc 模块方式时,Gradle 会妥善处理 buildSrc 模块的构建脚本与源码,并将其添加到当前项目的 classpath 中。但 buildSrc 方式的插件只能在项目内共享与复用,若要在其他项目中使用该插件,还需要再进行下列操作
- 将插件发布到 maven 仓库(任意仓库)
- 在需要应用该插件的构建脚本中的 repository 部分添加该插件所在的 maven 仓库
- 在需要应用该插件的构建脚本中的 classpath 部分添加该插件对应的 maven 坐标 (group : id : version)
因为是在其他项目中使用该项目 buildSrc 模块 中的自定义 Gradle 插件,所以 Gradle 的 buildSrc 保留目录优势不再。如果将模块名由 buildSrc 修改为其他名称,则可将其称为独立的 Gradle 插件模块。
2.使用buildSrc模块方式构建插件
由于我对maven仓库的发布不是很熟悉,所以我选用buildSrc方式构建插件。
1.在project内创建buildsrc目录
目录结构如下:
需要注意的是这里只是创建buildsrc目录就行,不是创建library,也不需要在setting.gradle中添加buildsrc模块。
src/main下建立groovy和java两个目录,整体目录结构如下:
2.build.gradle内容编写
(1) 因为gradle插件需要用groovy语言来编写,所以需要引用groovy插件:apply plugin: 'groovy'
(2) 由于buildsrc目录引用不到project的build配置,所以这里需要配置仓库的下载地址:
repositories {
google()
mavenCentral()
}
(3) 依赖项:
dependencies {
//依赖libs下的jar包,因为需要进行xml解析所以引入了dom4j的jar包
implementation fileTree(dir: 'libs', include: ['*.jar'])
//gradle sdk gradle相关sdk引用
implementation gradleApi()
//groovy sdk groovy相关sdk引用
implementation localGroovy()
//JavaPoet依赖
implementation 'com.squareup:javapoet:1.13.0'
}
3.用groovy编写插件
因为groovy是基于Java的因此他们的构建都很类似,且groovy文件可以引用的Java文件的类和方法,我们这次也是插件用groovy写,使用JavaPoet自动生成Java类的工作用Java编写。
(1) 为了让我们的groovy类申明为gradle插件,我们新建的groovy文件需要继承org.gradle.api.Plugin接口并实现apply方法;
示例:
class ViewBinding implements Plugin<Project> {
private static final String CONFIG_NAME = "viewBinding"
private static final String TASK_NAME = "generated_code"
@Override
void apply(Project project) {
System.out.println("插件开始")
}
}
(2) 添加viewBinding的enable扩展属性;
示例:
class ViewBindingConfig {
boolean enable
}
//添加扩展属性
def viewBinding = project.extensions.create(CONFIG_NAME, ViewBindingConfig.class)
(3) 定义插件任务,实现判断enable的值来决定是否需要生成Java类
示例:
project.task(TASK_NAME){
//manifest的目录
String manifest = project.getProjectDir().getAbsolutePath() + File.separator + "src" +
File.separator + "main" + File.separator + "AndroidManifest.xml"
//通过manifest获取项目包名
def packageName = XmlUtil.getRootElement(manifest)
.attribute(QName.get("package"))
.getValue()
System.out.println(String.format("featureName is %s,packageName is %s",project.getProjectDir().getAbsolutePath(),packageName))
//通过JavaPoet生成文件
GenerateViewBinding.execute(packageName as String,project, viewBinding.enable as boolean)
}
4.定义插件名
在main下创建resources目录,下面创建META-INF/gradle-plugins/com.mins.plugin.properties文件,注意下划线上的地方可以随便命名,不过这里就是插件名字了,后面使用的时候需要与这里保持一致。
com.mins.plugin.properties文件里面配置好你的插件入口
示例:
implementation-class=com.mins.plugin.ViewBinding
5. 使用插件
在你需要使用自定义插件的module的build.gradle的顶部引用插件id,内容中添加viewBinding enable为true就可以了。
示例:
plugins {
id 'com.android.application'
id 'com.mins.plugin'
}
viewBinding{
enable true
}
activity中:
com.mins.annotationtestapplication.app.ActivityMainBinding bing = com.mins.annotationtestapplication.app.ActivityMainBinding.setContentView(this, R.layout.activity_main);
bing.text.setText("哈哈");
三、问题总结:
-
make完成后并没有在app的/build/generated/ap_generated_sources/debug/out/viewBinding目录下生成对应文件。
解决办法:
1)可以从右侧gradle中找到app/Tasks/Other下的对应插件任务名generated_code,双击执行任务。
2)使用project.afterEvaluate,这个方法回调是在make执行完成。
示例:
//在工程make完成后执行里面的方法
project.afterEvaluate {
//manifest的目录
String manifest = project.getProjectDir().getAbsolutePath() + File.separator + "src" +
File.separator + "main" + File.separator + "AndroidManifest.xml"
//通过manifest获取项目包名
def packageName = XmlUtil.getRootElement(manifest)
.attribute(QName.get("package"))
.getValue()
System.out.println(String.format("featureName is %s,packageName is %s",project.getProjectDir().getAbsolutePath(),packageName))
//通过JavaPoet生成文件
GenerateViewBinding.execute(packageName as String,project, viewBinding.enable as boolean)
}
-
生成Java类的文件路径
开始我使用的是build/generated/source/viewbinding,结果在activity中引用不到类,后来发现需要手动设置目录为Generate Sources Root才能引用到。所以后来我把生成路径改为了系统默认文件夹就是Generate Sources Root类型的/build/generated/ap_generated_sources/debug/out目录下。

相关视频推荐:
【Android开发中高级教程】自定义View基础_哔哩哔哩_bilibili
【Android开发教程】UI绘制体系原理_哔哩哔哩_bilibili
【Android开发教程】今日头条UI结构与ViewPager原理分析与实战_哔哩哔哩_bilibili
【Android开发教程】高级UI训练营_哔哩哔哩_bilibili
对标阿里年薪70W+Android开发进阶合集,含项目实战、面试专题、源码解析以及开源框架解析,已更新至133集_哔哩哔哩_bilibili
Android面试合集——字节跳动、阿里、腾讯、百度等一线大厂2021年面试真题解析/HashMap/组件化/插件化/热修复/性能优化/JVM/事件分发_哔哩哔哩_bilibili