Jetpck 才是真的豪华全家桶
引言
- 通过视图绑定功能,可以更轻松地编写可与视图交互的代码。
- 在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。
- 绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。
- 在大多数情况下,视图绑定会替代 findViewById。
整体预览
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 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。比如上面的文件是 item_rv.xml,那么会生成ItemRvBindiing的绑定类。
绑定内容:生成对应的绑定类均包含对根视图以及具有 ID 的所有视图的引用。如果视图没有添加 ID,则不会生成对应的绑定类引用。
根视图:每个绑定类还包含一个 getRoot()
方法,用于为相应布局文件的根视图提供直接引用。比如上面的就是LinearLayout根视图。
1.2.2 layout文件忽略
...
构建优化 & 内存优化: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) : RecyclerView.Adapter() {
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
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对象
//标记了Targets信息,为了生成对应的JavaModel
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 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 全家桶