ViewBinding, do you really understand?

foreword

In Androiddevelopment, control binding is a long-standing topic.

At the beginning, it was used findViewById, and the screen was full of various finds;

Later, it appeared Butterknife, using annotations to bind controls, which made the code of the UI layer a lot cleaner. Even so, there are still many bloated global variable controls;

Later kotlin, the kotlin-android-extensions plugin was launched, which can directly use the id to get xmlthe control object and use it. This method is really delicious. Its principle is to use the bytecode instrumentation technology to help us automatically generate similar findViewByIdHere you can refer to Guoshen's blog:

The kotlin-android-extensions plugin is also deprecated? lift me up
blog.csdn.net/guolin_blog…

I guess the reason for the specific abandonment:

1. Not compatible Java. Although googlevarious new technologies are taking priority now java, such as coroutines Compose, but these can be independent of the platform, and the function of control binding is platform-based, and the javauser group must be considered.

2. Although we kotlin-android-extensionsare very cool to use, but some problems are also exposed from its implementation principle, which invisibly reduces the running efficiency of the program.

use

ViewBindingThe simplicity of use can be said to be very simple.

First configure it under moudleour build.gradle:

buildFeatures {
        viewBinding true
}
复制代码

viewbindingThe configuration is independent moudleof .

After configuration, the corresponding Binding class will be generated, and we can call it directly to bind it.

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

In use, we can get the controls defined in bindingour layout directly through it, which is very convenient.XML

principle

生成的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

image-20220405142934287

image-20220405142934287

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

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

AS右侧的gradle任务栏中,找到了关于databindingtask

image-20220405143309503

image-20220405143309503

我们再分别执行 dataBindingMergeDependencyArtifactsDebugdataBindingMergeGenClassesDebugdataBindingGenBaseClassesDebug

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

image-20220405143551157

image-20220405143551157

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

image-20220405143733504

image-20220405143733504

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'
复制代码

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

我们查找的入口主要有两个,一个AGPTaskManager 类,这个是管理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)
        }
    }
复制代码

这里首先获取配置信息,看看ViewBindingDataBinding 的开关状态,如果两个都是关闭直接返回。否则 进行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类文件对比,可以发现完全吻合。

So our ViewBindingclass file is javapoetgenerated here.

Summarize

Let's conclude by summarizing:

By observing the compilation process, we find that it dataBindingGenBaseClassesDebugis a generated bindingclass task, and then TaskManagerfind the corresponding

DataBindingGenBaseClassesTask, find the entry of execution through @TaskActionannotations task, and finally call to the DataBindinginside BaseDataBinderclass. In this process, through the ViewBindercall to the JavaFileGeneratorcreate() method of , here javapoetthe class we use is generated through Viewbinding.

The overall calling process:

TaskManager

->writeBaseClasses

->CodeGenerator :: run()

->BaseDataBinder::generateAll()

->ViewBinder::toJavaFile()

->JavaFileGenerator:: create()

->typeSpec()

->javapoet
复制代码

write at the end

The overall process is not complicated. After reading it, it is best for you to follow the source code yourself. This is done in person, and it is only when you understand it thoroughly.

Theory and practice can be truly learned.

Guess you like

Origin juejin.im/post/7083099770322944007