ViewBinding理解

299 篇文章 4 订阅
212 篇文章 8 订阅

1

前言

在Android开发中,控件绑定是一个久远的话题。

最开始就是使用findViewById,满屏都是各种find;

后来出现了Butterknife,使用注解来进行控件绑定,这样一来使UI层的代码清爽了很多,即使这样,还存在众多臃肿的全局变量的控件;

后来kotlin 推出了kotlin-android-extensions  插件,可以直接用id就能得到xml中的控件对象并且使用,这种方式真的很香,它原理是利用了字节码插桩技术,帮我们自动生成了类似findViewById的东西,这里可以参考一下郭神的博客:

kotlin-android-extensions插件也被废弃了?扶我起来

https://blog.csdn.net/guolin_blog/article/details/113089706

具体废弃的原因我猜测:

1.不兼容Java。虽然现在google各种新技术都在以java为主,像协程,Compose之类,但是这些都是可以独立于平台的,而控件绑定这个功能是基于平台的,必然需要考虑java用户群体。

2.虽然我们kotlin-android-extensions 我们使用起来非常爽,但是从它的实现原理也暴露出来一些问题,在无形之中降低了程序的运行效率。

2

使用

ViewBinding的简单使用可以说是非常简单。

首先在我们的moudle 的build.gradle下进行配置:

 
buildFeatures {
        viewBinding true
}

配置之后,会生成对应的Binding类,我们直接调用,进行绑定即可。

lateinit var binding: MainActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = MainActivityBinding.inflate(layoutInflater)
    setContentView(binding.root)
    binding.message.text = "Android开发那点事儿"
}
 

在使用上,我们就可以直接通过binding来获取到我们XML布局中定义的控件,非常方便。

3

原理

生成的binding文件 是在 build/generated/data_binding_base_class_source_out/debug/out 目录下,我们先看下生成的类内容

这是我定义的XML布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MainFragment"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
 

这是生成的binding类:

public final class MainActivityBinding implements ViewBinding {
  private final ConstraintLayout rootView;
  public final ConstraintLayout main;
  public final TextView message;
  private MainActivityBinding( ConstraintLayout rootView, ConstraintLayout main, TextView message) {
    this.rootView = rootView;
    this.main = main;
    this.message = message;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static MainActivityBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static MainActivityBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.main_activity, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static MainActivityBinding bind(@NonNull View rootView) {
    int id;
    missingId: {
      ConstraintLayout main = (ConstraintLayout) rootView;

      id = R.id.message;
      TextView message = ViewBindings.findChildViewById(rootView, id);
      if (message == null) {
        break missingId;
      }
      return new MainActivityBinding((ConstraintLayout) rootView, main, message);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}
 

在这里viewbinding 帮我解析的xml布局文件,并对设置Id的控件自动进行控件绑定,最后我们最后通过viewbinding获取根布局,调用setContent方法来添加布局。

@Override
public void setContentView(View v) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}

原来是由actiivty 进行布局解析,由我们自己进行控件绑定并使用,现在就相当于ViewBinding将这两件事情都做了。

那这些binding类是如何生成的呢?这就是接下来我们要探索的话题。

当我改变布局文件的时候,发现binding类文件并不会实时发生改变,需要编译之后binding类文件才会进行对应改变,由此推断,binding类文件应该是APG在编译项目的时候生成的。

我们运行一下,看下Task:

整个编译流程中,只捕捉到了三个关于dataBinding 的task,我们现在并不能确定是哪个task生成的binding类,那怎么办?

那我们就一个一个执行,一个一个去试。

在AS右侧的gradle任务栏中,找到了关于databinding 的task。

我们再分别执行 dataBindingMergeDependencyArtifactsDebug ,dataBindingMergeGenClassesDebug 和  dataBindingGenBaseClassesDebug 。

先clean操作,然后执行 前两个task 之后,发现只是生成了两个空文件夹,并未有内容生成:

 

在执行了dataBindingMergeGenClassesDebug 之后,生成了我们所需要的binding类, 

 

那dataBindingMergeGenClassesDebug 这个task 就是我们要探索的重点。

接下来我们引入AGP源码和Databinding源码,进行分析:

implementation 'com.android.tools.build:gradle:7.0.2'
implementation 'androidx.databinding:databinding-compiler-common:7.0.2'
implementation 'androidx.databinding:databinding-common:7.0.2'
implementation 'com.android.databinding:baseLibrary:7.0.2'
 

在app的build.gradle中引入就行,我们在External Libraries 中进行查阅。

我们查找的入口主要有两个,一个AGP的 TaskManager 类,这个是管理Task创建的类,还有另外一个入口,就是从 /com/android/build/gradle/internal/tasks 路径去找,这个是属于熟能生巧的一个捷径。

首先看TaskManager ,通过方法查阅,我们会发现关于创建DataBinding Task 的只要一个方法 createDataBindingTasksIfNecessary。

protected fun createDataBindingTasksIfNecessary(creationConfig: ComponentCreationConfig) {
    val dataBindingEnabled = creationConfig.buildFeatures.dataBinding
    val viewBindingEnabled = creationConfig.buildFeatures.viewBinding
    if (!dataBindingEnabled && !viewBindingEnabled) {
        return
    }
    taskFactory.register(DataBindingMergeBaseClassLogTask.CreationAction(creationConfig))
    taskFactory.register(
            DataBindingMergeDependencyArtifactsTask.CreationAction(creationConfig))
    DataBindingBuilder.setDebugLogEnabled(logger.isDebugEnabled)
    taskFactory.register(DataBindingGenBaseClassesTask.CreationAction(creationConfig))

    // DATA_BINDING_TRIGGER artifact is created for data binding only (not view binding)
    if (dataBindingEnabled) {
        if (projectOptions[BooleanOption.NON_TRANSITIVE_R_CLASS]
                && isKotlinKaptPluginApplied(project)) {
            // TODO(183423660): Undo this workaround for KAPT resolving files at compile time
            taskFactory.register(MergeRFilesForDataBindingTask.CreationAction(creationConfig))
        }
        taskFactory.register(DataBindingTriggerTask.CreationAction(creationConfig))
        setDataBindingAnnotationProcessorParams(creationConfig)
    }
}
 

这里首先获取配置信息,看看ViewBinding 和 DataBinding 的开关状态,如果两个都是关闭直接返回。否则 进行Task注册,在这里进行注册的task,有两个是我们 在编译过程中看到的task,再继续,就是当databinding 开启的时候,会再额外 注册task ,通过这里我们可以了解到 viewBinding 只是DataBinding 中的一部分功能。viewbinding只是进行控件绑定,DataBinding除了基础的控件绑定之外,还拥有双向数据绑定等功能。

接下来我们看看DataBindingGenBaseClassesTask。

这个Task类的路径是  com.android.build.gradle.internal.tasks.databinding ,跟我说提到的第二个入口吻合,所以以后分析AGP源码,可以从这个路径来找对应的Task,这是一种取巧的方式。

写过自定义插件的朋友都知道,自定义Task中 需要用注解 @TaskAction 来标识一下task 的运行入口。

@TaskAction
fun writeBaseClasses(inputs: IncrementalTaskInputs) {
      recordTaskAction(analyticsService.get()) {
        val args = buildInputArgs(inputs)
        CodeGenerator(
            args,
            sourceOutFolder.get().asFile,
            Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
            encodeErrors,
            collectResources()).run()
    }
}
 

可以看到 writeBaseClasses 方法被  @TaskAction 注解标识,那么这就是我们分析的入口。

这里主要是创建了  CodeGenerator 类,然后执行了 run()方法。

override fun run() {
    try {
        initLogger()
        BaseDataBinder(
                LayoutInfoInput(args),
                if (symbolTables != null) this::getRPackage else null)
            .generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath))
    } finally {
        clearLogger()
    }
}
 

在CodeGenerator:: run() 中,我们看到这里又创建了BaseDataBinder类,并运行了 generateAll 方法。

fun generateAll(writer : JavaFileWriter) {
..............
layoutBindings.forEach { layoutName, variations ->
    ...........
    if (variations.first().isBindingData) {
        check(input.args.enableDataBinding) {
            "Data binding is not enabled but found data binding layouts: $variations"
        }
        val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
        javaFile = binderWriter.write()
        classInfo = binderWriter.generateClassInfo()
    } else {
        check(input.args.enableViewBinding) {
            "View binding is not enabled but found non-data binding layouts: $variations"
        }
        val viewBinder = layoutModel.toViewBinder()
        javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
        classInfo = viewBinder.generatedClassInfo()
    }
 .................
}
 

这里对代码进行了精简,这里首先做了一个判断,判断是否为DataBinding ,很明显我们需要分析的内容 在else 里面。

在else 里面,先判断了viewBinding是否开启,然后将 BaseLayoutModel 对象转化为了 ViewBinder 对象,接下来执行了 ViewBinder的

拓展方法  toJavaFile ,这个方法名的意思就很明显了,是去转化为Java文件的。

fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
    JavaFileGenerator(this, useLegacyAnnotations).create()
 

这里是创建了JavaFileGenerator 类 ,执行create() 方法。

fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
    addFileComment("Generated by view binder compiler. Do not edit!")
}

这里这就是创建binding类的方法了 ,我们主要看下 typeSpec() 方法:

private fun typeSpec() = classSpec(binder.generatedTypeName) {
    增加 public final 修饰
    addModifiers(PUBLIC, FINAL)
    实现 ViewBinding 接口 
    addSuperinterface(ClassName.get(viewBindingPackage, "ViewBinding"))
    添加 rootView 变量
    addField(rootViewField())
    添加 控件 变量
    addFields(bindingFields())
    创建 无参构造方法
    addMethod(constructor())
    创建 根布局的 get方法
    addMethod(rootViewGetter())

    if (binder.rootNode is RootNode.Merge) {
        addMethod(mergeInflate())
    } else {
        创建一个参数的 inflate 方法
        addMethod(oneParamInflate())
        创建三个参数的 inflate 方法 
        addMethod(threeParamInflate())
    }
    添加 bind 方法
    addMethod(bind())
}
 

使用过javapoet的同学可以看出 这就是使用javapoet 来创建java文件。

经过 这些创建流程 与我们生成的viewbinding类文件对比,可以发现完全吻合。

所以我们的ViewBinding类文件 就是在这里通过javapoet生成的。

4

总结

我们最后总结一下:

我们通过观察编译流程,得出dataBindingGenBaseClassesDebug 是生成binding类的task,然后通过TaskManager 找到对应的

DataBindingGenBaseClassesTask ,通过@TaskAction 注解找到task执行的入口,最后调用到 DataBinding 里面 BaseDataBinder 类,在这个过程中,通过 ViewBinder 调用到了  JavaFileGenerator 的 create() 方法,在这里通过javapoet 生成了我们所使用Viewbinding类。

 

整体调用流程:

TaskManager

->writeBaseClasses

->CodeGenerator :: run()

->BaseDataBinder::generateAll()

->ViewBinder::toJavaFile()

->JavaFileGenerator:: create()

->typeSpec()

->javapoet
 

5

写在最后

整体流程并不是算复杂,大家在阅读后最好还是自己去跟一遍源码,这个亲自跟一遍,自己理解的才算透彻。

转自:ViewBinding,你真的理解了吗?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值