Jetpack ViewBinding

1,372 阅读8分钟

Jetpck 才是真的豪华全家桶

引言

  • 通过视图绑定功能,可以更轻松地编写可与视图交互的代码。
  • 在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。
  • 绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。
  • 在大多数情况下,视图绑定会替代 findViewById。

整体预览

Jetpack ViewBinding 概览图

1. 使用说明

1.1 环境配置

1.1.1 版本要求

 ViewBinding 在 Android Studio 3.6 Canary 11 及更高版本中可用。

1.1.2 模块启用

//build.gradle
android {
        buildFeatures {
            viewBinding true
        }
    }

1.2 语法说明

1.2.1 layout布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

生成规则:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。比如上面的文件是 item_rv.xml,那么会生成ItemRvBindiing的绑定类。  绑定内容:生成对应的绑定类均包含对根视图以及具有 ID 的所有视图的引用。如果视图没有添加 ID,则不会生成对应的绑定类引用。  根视图:每个绑定类还包含一个 getRoot() 方法,用于为相应布局文件的根视图提供直接引用。比如上面的就是LinearLayout根视图。

1.2.2 layout文件忽略

<LinearLayout
            ...
            tools:viewBindingIgnore="true" >
        ...
    </LinearLayout>

 构建优化 & 内存优化:ViewBinding的开启会对所有的布局文件进行绑定类生成,如果有些布局文件不需要绑定类生成, 则可以在根布局添加设置进行关闭。  Apk瘦身优化:在release版本,可以添加 shrinkResources 将没有用到的绑定类不打包在apk中。

1.3 场景举例

1.3.1 Activity

class ViewBindingActivity : AppCompatActivity() {

    private lateinit var binding: ActivityViewBindingBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //传统方式
//        setContentView(R.layout.activity_view_binding)

        //ViewBinding方式
        binding = ActivityViewBindingBinding.inflate(layoutInflater);
        val view = binding.root
        setContentView(view)

        //ViewBinding操作演示
        binding.rename.setOnClickListener {
            binding.name.text = binding.name.text.toString() + "-rename"
        }
    }
}

三步走

  • 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Activity 使用。
  • 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
  • 将根视图传递到 setContentView(),使其成为屏幕上的活动视图。

1.3.2 Fragment

class ViewBindingFragmentSample : Fragment() {
    private var _binding: FragmentViewBindingBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        //传统方式
//        return inflater.inflate(R.layout.fragment_view_binding, container, false)

        //style 1
        _binding = FragmentViewBindingBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //style 2(需要调用onCreateView中的传统方式)
//        _binding = FragmentViewBindingBinding.bind(view)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

三步走

  • 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Fragment 使用。
  • 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
  • onCreateView() 方法返回根视图(也可以在onViewCreated()进行bind()),使其成为屏幕上的活动视图。

1.3.3 ViewHolder

class RvAdapter(private val mData : List<String>) : RecyclerView.Adapter<RvAdapter.RvViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RvViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = ItemRvTextBinding.inflate(inflater)
        return RvViewHolder(binding)
    }

    override fun getItemCount(): Int {
        return mData.size
    }

    override fun onBindViewHolder(holder: RvViewHolder, position: Int) {
        holder.getBinding().title.text = mData[position]
    }

    class RvViewHolder(private val binding: ItemRvTextBinding) : RecyclerView.ViewHolder(binding.root) {
        fun getBinding() : ItemRvTextBinding {
            return binding
        }
    }
}

三步走

  • 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 ViewHolder 使用。
  • onCreateViewHolder创建 ViewHolder,使其成为屏幕上的活动视图。
  • onBindViewHolder() 进行绑定类的属性获取更新。

1.4 ViewBinding类

1.4.1 文件说明

1.4.1.1 绑定类路径

JavaModel:app/build/generated/data_binding_base_class_source_out/buildTypes/out/packageName/databinding

绑定类生成路径

Layout文件:app/build/intermediates/data_binding_layout_info_type_merge/buildTypes/out

Layout文件生成路径

1.4.1.2 绑定类内容

JavaModel

  • 作用1:根据layout文件生成视图,并与父布局关联(可能)。
  • 作用2:根据生成的视图,绑定子视图组件的引用。
public final class ActivityViewBindingBinding implements ViewBinding {
  @NonNull
  private final LinearLayout rootView;   //根对象

  @NonNull
  public final LinearLayout detail;  //只要xml写了ID,那么就会生成对应的引用

  @NonNull
  public final TextView name;

  @NonNull
  public final Button rename;

  @NonNull
  public final RecyclerView rv;

  //私有构造函数,只能用在下面的 bind()方法中,符合最少知道原则
  private ActivityViewBindingBinding(@NonNull LinearLayout rootView, @NonNull LinearLayout detail,
      @NonNull TextView name, @NonNull Button rename, @NonNull RecyclerView rv) {
    this.rootView = rootView;
    this.detail = detail;
    this.name = name;
    this.rename = rename;
    this.rv = rv;
  }

  @Override
  @NonNull
  //根布局可获取
  public LinearLayout getRoot() {
    return rootView;
  }

  @NonNull
  //inflate 重载方法
  public static ActivityViewBindingBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  //inflate 重载方法:生成视图并绑定子视图组件的引用
  public static ActivityViewBindingBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    //生成视图
    View root = inflater.inflate(R.layout.activity_view_binding, parent, false);
    if (attachToParent) { //是否添加父布局。tip:这个会影响这个layout文件的根视图参数是否生效
      parent.addView(root);
    }
    return bind(root); //绑定子视图组件的引用
  }

  @NonNull
  //绑定子视图组件的引用
  public static ActivityViewBindingBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.detail;
      //回归原始,依然通过 findViewById 获取视图,并保存保存引用
      LinearLayout detail = rootView.findViewById(id);
      if (detail == null) {
        break missingId;
      }

      id = R.id.name;
      TextView name = rootView.findViewById(id);
      if (name == null) {
        break missingId;
      }

      id = R.id.rename;
      Button rename = rootView.findViewById(id);
      if (rename == null) {
        break missingId;
      }

      id = R.id.rv;
      RecyclerView rv = rootView.findViewById(id);
      if (rv == null) {
        break missingId;
      }

      //返回最终的绑定类
      return new ActivityViewBindingBinding((LinearLayout) rootView, detail, name, rename, rv);
    }
    //视图解析错误异常抛出
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

Layout文件

  • 作用:为了生成JavaModel对象
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="app/src/main/res/layout/activity_view_binding.xml"
    //因为只启用了ViewBinding,所以isBindingData是false
    isBindingData="false" isMerge="false" layout="activity_view_binding"
    modulePackage="com.kejiyuanren.jetpack" rootNodeType="android.widget.LinearLayout">
    //标记了Targets信息,为了生成对应的JavaModel
    <Targets>
        <Target tag="layout/activity_view_binding_0" view="LinearLayout">
            <Expressions />
            <location endLine="57" endOffset="14" startLine="1" startOffset="0" />
        </Target>
        <Target id="@+id/name" view="TextView">
            <Expressions />
            <location endLine="14" endOffset="36" startLine="9" startOffset="4" />
        </Target>
        <Target id="@+id/detail" view="LinearLayout">
            <Expressions />
            <location endLine="43" endOffset="18" startLine="22" startOffset="4" />
        </Target>
        <Target id="@+id/rename" view="Button">
            <Expressions />
            <location endLine="41" endOffset="35" startLine="36" startOffset="8" />
        </Target>
        <Target id="@+id/rv" view="androidx.recyclerview.widget.RecyclerView">
            <Expressions />
            <location endLine="55" endOffset="41" startLine="51" startOffset="4" />
        </Target>
    </Targets>
</Layout>

1.4.2 ViewBinding类 原理分析

 分析入口:从databinding-compiler进行分析,因为在编译环节会被调用。

分析入口

1.4.2.1 从何而来?

结论:利用res/layout中的xml文件,解析生成元素对象,进行缓存。

(1)LayoutXmlProcessor -> processResources()

public boolean processResources(final ResourceInput input)
            throws ParserConfigurationException, SAXException, XPathExpressionException,
            IOException {
        ……
        //核心处理
        ProcessFileCallback callback = new ProcessFileCallback() {
        ……
        if (input.isIncremental()) {
            //增量编译
            processIncrementalInputFiles(input, callback);
        } else {
            //全量编译
            processAllInputFiles(input, callback);
        }
        ……
    }

(2)LayoutXmlProcessor -> processAllInputFiles()

private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback)
            throws IOException, XPathExpressionException, SAXException,
            ParserConfigurationException {
        ……
        for (File firstLevel : input.getRootInputFolder().listFiles()) {
            if (firstLevel.isDirectory()) {   //是否是路径
                //文件夹是否为layout开头
                if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
                    //创建对应文件夹
                    callback.processLayoutFolder(firstLevel);
                    //noinspection ConstantConditions
                    //遍历文件夹中的xml文件
                    for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
                        //处理布局文件
                        callback.processLayoutFile(xmlFile);
                    }
                } else {
        ……
    }

(3)LayoutXmlProcessor -> processSingleFile()

public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output)
            throws ParserConfigurationException, SAXException, XPathExpressionException,
            IOException {
        //解析xml文件
        final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
                .parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup);
        if (bindingLayout != null && !bindingLayout.isEmpty()) {
            //解析出来的元素对象进行缓存
            mResourceBundle.addLayoutBundle(bindingLayout, true);
            return true;
        }
        return false;
    }

(4)LayoutFileParser -> parseXml()

public static ResourceBundle.LayoutFileBundle parseXml(@NonNull final RelativizableFile input,
            @NonNull final File outputFile, @NonNull final String pkg,
            @NonNull final LayoutXmlProcessor.OriginalFileLookup originalFileLookup)
            throws ParserConfigurationException, IOException, SAXException,
            XPathExpressionException {
        ……
            //解析继续
            return parseOriginalXml(
                RelativizableFile.fromAbsoluteFile(originalFile, input.getBaseDir()),
                pkg, encoding);
        ……
    }

(5)LayoutFileParser -> parseOriginalXml()

private static ResourceBundle.LayoutFileBundle parseOriginalXml(
            @NonNull final RelativizableFile originalFile, @NonNull final String pkg,
            @NonNull final String encoding)
            throws IOException {
        ……
            //文件解析(这个是databing用的,viewbinding的话在实现中直接return null)
            XMLParser.ElementContext data = getDataNode(root);
            XMLParser.ElementContext rootView = getViewNode(original, root);
           ……
            //生成元素对象
            ResourceBundle.LayoutFileBundle bundle =
                new ResourceBundle.LayoutFileBundle(
                    originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
                    isMerge);
            final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
            //数据解析(这个是databing用的,viewbinding的话在实现中直接return null)
            parseData(original, data, bundle);
            parseExpressions(newTag, rootView, isMerge, bundle);
            return bundle;
        ……
    }

1.4.2.2 途径哪里?

结论:利用上一节生成的元素对象缓存,解析生成中间件layout文件(build目录下的xml文件)。

(1)LayoutXmlProcessor -> writeLayoutInfoFiles()

//
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
        //元素对象集合遍历
        for (List<ResourceBundle.LayoutFileBundle> layouts : mResourceBundle.getLayoutBundles()
                .values()) {
            for (ResourceBundle.LayoutFileBundle layout : layouts) {
                //利用元素对象缓存生成layout文件
                writeXmlFile(writer, xmlOutDir, layout);
            }
        }
        ……
    }

(2)LayoutXmlProcessor -> writeXmlFile()

private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,
            ResourceBundle.LayoutFileBundle layout)
            throws JAXBException {
        //生成文件名
        String filename = generateExportFileName(layout);
        //写文件
        writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
    }

1.4.2.3 去往何处?

结论:利用上一节生成的中间件layout文件,解析生成ViewBinding类。

(1)BaseDataBinder -> init()

init {
        input.filesToConsider
                .forEach {
                    it.inputStream().use {
                         // 将中间件layout中的xml文件 转成 LayoutFileBundle
                        val bundle = ResourceBundle.LayoutFileBundle.fromXML(it)
                        // 缓存进 ResourceBundle
                        resourceBundle.addLayoutBundle(bundle, true)
                    }
        ……
    }

(2)BaseDataBinder -> generateAll()

fun generateAll(writer : JavaFileWriter) {
        ……
        //根据文件名进行分组排序,并进行遍历所有ResourceBundle
        resourceBundle.layoutFileBundlesInSource.groupBy { it.mFileName }.forEach {
            val layoutName = it.key
            val layoutModel = BaseLayoutModel(it.value)
            //BaseLayoutBinderWriter生成
            val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
            //BaseLayoutBinderWriter的解析处理:binderWriter.write(),并写文件:writer.writeToFile()
            writer.writeToFile(binderWriter.write())
            ……
    }

(3)BaseLayoutBinderWriter -> write()

//解析数据: createType(),并生成JavaFile:javaFile()
fun write() = javaFile(binderTypeName.packageName(), createType()) {
        addFileComment("Generated by data binding compiler. Do not edit!")
    }

(4)BaseLayoutBinderWriter -> createType()

//解析所有,细节就不跟了
private fun createType() = classSpec(binderTypeName) {
        superclass(viewDataBinding)
        addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
        addFields(createBindingTargetFields())
        addFields(createVariableFields())
        addMethod(createConstructor())
        addMethods(createGettersAndSetters())
        addMethods(createStaticInflaters())
    }

原理分析总结

  • Step1:layout文件(xml资源文件) -> 解析 -> 元素缓存 。
  • Step2:元素缓存 -> 解析 -> layout文件(中间件) 。
  • Step3:layout文件(中间件) -> 解析 -> ViewBinding类 。

2. 横向对比

2.1 findViewById

与使用 findViewById 相比,视图绑定具有一些很显著的优点:

  • Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。
  • 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。

这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。

2.2 DataBinding

视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势

  • 更快的编译速度:视图绑定不需要处理注释,因此编译时间更短。
  • 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。

反过来,与数据绑定相比,视图绑定也具有以下限制

  • 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。
  • 视图绑定不支持双向数据绑定。

推荐:在某些情况下,最好在项目中同时使用视图绑定和数据绑定。可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。

3. 小结

 ViewBinding相比DataBinding更轻量级,不是每次杀鸡都需要用牛刀。尽快把 findViewById 忘了吧!

小编的博客系列

Jetpack 全家桶