今天使用 ViewBinding 时遇到一个 Crash:java.lang.NullPointerException: Missing required view with ID,最终发现是与自定义 View 有关系……

一、背景

最近使用 ViewBinding 时,遇到这么一个报错:

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: me.hjhl.app, PID: 10740
E AndroidRuntime: java.lang.NullPointerException: Missing required view with ID: me.hjhl.app:id/my_gl_surface_view
E AndroidRuntime:   at me.hjhl.app.databinding.FragmentGlesDemoBinding.bind(FragmentGlesDemoBinding.java:67)
E AndroidRuntime:   at me.hjhl.app.databinding.FragmentGlesDemoBinding.inflate(FragmentGlesDemoBinding.java:49)
E AndroidRuntime:   at me.hjhl.app.demo.GLESDemoFragment.onCreateView(GLESDemoFragment.kt:31)
E AndroidRuntime:   at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2995)
E AndroidRuntime:   at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:523)
E AndroidRuntime:   at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1840)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1764)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1701)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2849)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2784)
E AndroidRuntime:   at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:262)
E AndroidRuntime:   at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:478)
E AndroidRuntime:   at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:246)
E AndroidRuntime:   at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1433)
E AndroidRuntime:   at android.app.Activity.performStart(Activity.java:7923)
E AndroidRuntime:   at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3337)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
E AndroidRuntime:   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2049)
E AndroidRuntime:   at android.os.Handler.dispatchMessage(Handler.java:107)
E AndroidRuntime:   at android.os.Looper.loop(Looper.java:228)
E AndroidRuntime:   at android.app.ActivityThread.main(ActivityThread.java:7589)
E AndroidRuntime:   at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime:   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
E AndroidRuntime:   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:953)

整体代码逻辑大致是:Activity 创建时跳转到一个 Fragment,这个 Fragment 对应的 XML 里很简单——FrameLayout 里套了一个自定义 View。错误堆栈提示找不到资源 id my_gl_surface_view。这让我不禁怀疑是不是自定义 View 导致的。

二、分析过程

2.1 找到 ViewBinding 生成的 UI 类

ViewBinding 的大致原理是在编译时,把开启了 ViewBinding 功能的布局资源 XML 生成对应的 Java 类。例如在这个 case 中,对应的类文件位置为:app/build/generated/data_binding_base_class_source_out/debug/out/me/ljh/app/databinding/FragmentGlesDemoBinding.java

2.2 分析关键代码

以下代码片段是从 FragmentGlesDemoBinding 中摘出:

// file: app/build/generated/data_binding_base_class_source_out/debug/out/me/ljh/app/databinding/FragmentGlesDemoBinding.java
public final class FragmentGlesDemoBinding implements ViewBinding {@NonNullpublic static FragmentGlesDemoBinding 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.my_gl_surface_view;MyGLSurfaceView myGlSurfaceView = ViewBindings.findChildViewById(rootView, id);if (myGlSurfaceView == null) {// 无法找到 view,则 break 准备抛出异常break missingId;}// 递归处理 R.id.my_gl_surface_view 的子 viewreturn new FragmentGlesDemoBinding((FrameLayout) rootView, myGlSurfaceView);}String missingId = rootView.getResources().getResourceName(id);throw new NullPointerException("Missing required view with ID: ".concat(missingId));}
}

考虑到该问题跟布局文件密切相关,放上布局文件的源码:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="?attr/fullscreenBackgroundColor"android:theme="@style/ThemeOverlay.LearnAndroidOpenGL.FullscreenContainer"tools:context=".demo.GLESDemoFragment"><!-- 这里使用了自定义布局 --><me.ljh.app.widget.MyGLSurfaceViewandroid:id="@+id/my_gl_surface_view"android:layout_width="match_parent"android:layout_height="match_parent" /></FrameLayout>

其中,MyGLSurfaceView 的实现如下:

class MyGLSurfaceView(context: Context, attrs: AttributeSet?) : GLSurfaceView(context) {// MyGLRender 是继承自 GLSurfaceView.Render 接口的一个类,与这里的报错并无关系private val renderer: MyGLRender = MyGLRender()init {setEGLContextClientVersion(3)setRenderer(renderer)renderMode = RENDERMODE_WHEN_DIRTY}constructor(context: Context) : this(context, null)
}

2.2.1 怀疑点1:与 ViewBinding 不支持布局中有自定义的 View 控件有关?

虽然觉得不可能,但还是验证了下:

class GLESDemoFragment : Fragment() {private var _binding: FragmentGlesDemoBinding? = nullprivate val mBinding get() = _binding!!override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View {// 步骤一:将 ViewBinding 的代码注释掉//_binding = FragmentGlesDemoBinding.inflate(inflater, container, false)// return mBinding.root// 步骤二:使用 inflate XML 的方式加载 Viewval rootView = inflater.inflate(R.layout.fragment_gles_demo, container, false)return rootView}
}

意外发现,这样竟然不会奔溃,可以运行了!

不会吧,真与 ViewBinding 有关?抱着怀疑、实事求是的态度,尝试在上面的基础上,通过 findViewById 拿到 view 实例。

val rootView = inflater.inflate(R.layout.fragment_gles_demo, container, false)
val myGlView = rootView.findViewById(R.id.my_gl_surface_view)
Log.d(TAG, "my gl view $myGlView")
return rootView

果不其然,竟然获取到的为 null !!

这说明在根 view 中,找不到 ID 为 my_gl_surface_view 的子 view,但从这并不复杂的 XML 布局中,可以清晰看到,ID 确确实实是对的,哪里错了呢?并且,为什么通过手动 inflate 布局 XML 的方式,程序能运行的好好地呢?

2.3 发现问题

考虑到用了自定义 View,这种情况下,怀疑大概是自定义 View 写的有问题。

回到 MyGLSurfaceView 的源码,突然注意到它的构造函数

class MyGLSurfaceView(context: Context, attrs: AttributeSet?) : GLSurfaceView(context) {// ......constructor(context: Context) : this(context, null)
}

最开始自定义 View 的时候,主构造函数只用到了一个 Context,然后发现在 XML 中引入布局后,运行程序会发生奔溃。

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: me.ljh.app, PID: 16008
E AndroidRuntime: android.view.InflateException: Binary XML file line #13 in me.ljh.app:layout/fragment_gles_demo: Binary XML file line #13 in me.ljh.app:layout/fragment_gles_demo: Error inflating class me.ljh.app.widget.MyGLSurfaceView
E AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #13 in me.ljh.app:layout/fragment_gles_demo: Error inflating class me.ljh.app.widget.MyGLSurfaceView
E AndroidRuntime: Caused by: java.lang.NoSuchMethodException: me.ljh.app.widget.MyGLSurfaceView.<init> [class android.content.Context, interface android.util.AttributeSet]
E AndroidRuntime:   at java.lang.Class.getConstructor0(Class.java:2332)
E AndroidRuntime:   at java.lang.Class.getConstructor(Class.java:1728)
E AndroidRuntime:   at android.view.LayoutInflater.createView(LayoutInflater.java:828)
E AndroidRuntime:   at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1010)
E AndroidRuntime:   at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:965)
E AndroidRuntime:   at android.view.LayoutInflater.rInflate(LayoutInflater.java:1127)
E AndroidRuntime:   at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)
E AndroidRuntime:   at android.view.LayoutInflater.inflate(LayoutInflater.java:686)
E AndroidRuntime:   at android.view.LayoutInflater.inflate(LayoutInflater.java:538)
E AndroidRuntime:   at me.ljh.app.demo.GLESDemoFragment.onCreateView(GLESDemoFragment.kt:34)
E AndroidRuntime:   at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2995)
E AndroidRuntime:   at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:523)
E AndroidRuntime:   at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1840)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1764)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1701)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2849)
E AndroidRuntime:   at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2784)
E AndroidRuntime:   at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:262)
E AndroidRuntime:   at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:478)
E AndroidRuntime:   at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:246)
E AndroidRuntime:   at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1433)
E AndroidRuntime:   at android.app.Activity.performStart(Activity.java:7923)
E AndroidRuntime:   at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3337)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
E AndroidRuntime:   at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
E AndroidRuntime:   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2049)
E AndroidRuntime:   at android.os.Handler.dispatchMessage(Handler.java:107)
E AndroidRuntime:   at android.os.Looper.loop(Looper.java:228)
E AndroidRuntime:   at android.app.ActivityThread.main(ActivityThread.java:7589)
E AndroidRuntime:   at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime:   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
E AndroidRuntime:   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:953)

因此,给 MyGlSurfaceView 的主构造函数加入了 AttributeSet 参数,便没有了 Crash。但并没有把该参数传递给父类 GLSurfaceView

猜测很大概率是这里,因此加上后尝试下。

class MyGLSurfaceView(context: Context, attrs: AttributeSet?) : GLSurfaceView(context, attrs) {// ......constructor(context: Context) : this(context, null)
}

通过 findViewById 能拿到 view 实例了,并且使用 ViewBinding 也正常了

看来真是这里的问题,但新问题来了,

  1. 这个参数是干嘛用的?
  2. 为什么 XML 方式创建 View 时一定需要它?
  3. 为什么没有这个参数,findViewById 会找不到 View?如果确实要这样,有其他办法可以拿到吗?
  4. 为什么自定义 View,通过 setContenxtView(MyGLSurfaceView(this)) 类似的方式可以正常使用?

三、探究原理

3.1 AttributeSet 是什么?

在 AS 中,对着 AttributeSetCtrl + B (Windows) / Command + B (MacOS),跳转到它的定义。

/*** A collection of attributes, as found associated with a tag in an XML* document.  Often you will not want to use this interface directly, instead* passing it to {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)* Resources.Theme.obtainStyledAttributes()}* which will take care of parsing the attributes for you.  In particular,* the Resources API will convert resource references (attribute values such as* "@string/my_label" in the original XML) to the desired type* for you; if you use AttributeSet directly then you will need to manually* check for resource references* (with {@link #getAttributeResourceValue(int, int)}) and do the resource* lookup yourself if needed.  Direct use of AttributeSet also prevents the* application of themes and styles when retrieving attribute values.* * <p>This interface provides an efficient mechanism for retrieving* data from compiled XML files, which can be retrieved for a particular* XmlPullParser through {@link Xml#asAttributeSet* Xml.asAttributeSet()}.  Normally this will return an implementation* of the interface that works on top of a generic XmlPullParser, however it* is more useful in conjunction with compiled XML resources:* * <pre>* XmlPullParser parser = resources.getXml(myResource);* AttributeSet attributes = Xml.asAttributeSet(parser);</pre>* * <p>The implementation returned here, unlike using* the implementation on top of a generic XmlPullParser,* is highly optimized by retrieving pre-computed information that was* generated by aapt when compiling your resources.  For example,* the {@link #getAttributeFloatValue(int, float)} method returns a floating* point number previous stored in the compiled resource instead of parsing* at runtime the string originally in the XML file.* * <p>This interface also provides additional information contained in the* compiled XML resource that is not available in a normal XML file, such* as {@link #getAttributeNameResource(int)} which returns the resource* identifier associated with a particular XML attribute name.** @see XmlPullParser*/

一大串有点长,总结就是:这是 XML 里面,对一个 View 配置的属性集合。比如android:id="@+id/my_gl_surface_view"android:layout_width="match_parent"android:layout_height="match_parent",它们会被 XML Parser 解析成一个个 Java 对象,AttributeSet 就是这些对象的集合。

这就回答了前面的 1、2、3、4 个问题:

  1. 这个参数,用于 XML 转换为 Java 对象时,传递 XML 中关于 view 实例的描述信息,包括它的id、宽、高……
  2. 显然,通过 XML 绘制 View 的方式一定会有这个参数。
  3. 没有这个参数,通过 android:id="@+id/my_gl_surface_view 方式指定的 id 自然就不会对 view 生效;如果一定要获取呢?既然能拿到父 view,就肯定有办法。我目前想到的是通过 rootView.children 的方式去遍历获取子 view 对象。但说实话没必要这样折腾自己。
  4. 显然,通过代码的方式,当然可以不提供了。

四、结束语

感觉好快就破案了,貌似有些潦草……

其实这问题在于,要理解 XML 到 UI 的原理,如果理解了,那就不会很难。

未正确自定义 View 导致 ViewBinding 使用 Crash:Missing required view with ID相关推荐

  1. java.lang.NullPointerException: Missing required view with报错

    一,报错信息如下 E/AndroidRuntime: FATAL EXCEPTION: mainProcess: com.example.androidmqttclient, PID: 8997jav ...

  2. Android 集成子模块 Missing required view with ID: com.kvdmoviesworld.android:id/

    记录问题: 在把一个app作为子模块集成入另一个app项目后,集成成功且编译通过,但是在运行时出现找不到view或者ID为空,我遇到的是主app项目中存在和子模块的布局文件有同名文件的存在. 例如:a ...

  3. 【记录】IDEA未正确关闭导致打开报错,进不了主界面,含解决办法

    [记录]IDEA未正确关闭导致打开报错,进不了主界面,含解决办法 错误提示 解决方案 参考 错误提示 截取了错误的主要部分 java.util.concurrent.CompletionExcepti ...

  4. 谷歌浏览器提示“要恢复页面吗?Chrome未正确关闭”

    谷歌chrome浏览器每次打开提示"要恢复页面吗"怎么办? 如下图所示: 001 每次打开启动谷歌chrome浏览器时都有这样的提示,让人烦不胜烦.点击恢复则会恢复你在上一次打开的 ...

  5. 配置发布和禁用复制功能时提示 分发服务器未正确安装。

    错误提示(配置发布和禁用复制功能时提示 分发服务器未正确安装.): 1.无法为本地服务器找到分发服务器或分发数据库.可能未安装分发服务器,也可能在分发服务器上未将本地服务器配置为发布服务器. 2.SQ ...

  6. java ldap 分页_具有从属引用的 LDAP 分页查询未正确处理

    具有从属引用的 LDAP 分页查询未正确处理 09/14/2020 本文内容 本文提供了一些方法来避免使用从属引用的 LDAP 分页查询未正确处理的问题. 原始产品版本:   Windows 8 原始 ...

  7. 金蝶K/3产品各版本引入/引出Excel文件时出现意外错误的提示,或未正确安装Excel的提示。微软补丁解决方案!(转)

    转自金蝶服务号 [1]问题回顾 2017年10月份起,微软陆续发布了各版本操作系统补丁,但更新补丁后,会导致金蝶K/3产品各版本引入/引出Excel文件时出现意外错误的提示,或未正确安装Excel的提 ...

  8. 金蝶K/3产品各版本引入/引出Excel文件时出现意外错误的提示,或未正确安装Excel的提示

    10月份起,微软陆续发布了各版本操作系统补丁,但更新补丁后,会导致金蝶K/3产品各版本引入/引出Excel文件时出现意外错误的提示,或未正确安装Excel的提示, 经过金蝶K/3产品研发与服务支持部门 ...

  9. 戴尔台式机修复计算机,dell电脑win10自动修复你的电脑未正确启动怎么修复

    在众多地品牌电脑中,许多用户也都会选择自己喜欢的dell电脑来安装win10系统,但是最近有些用户在使用win10系统自动修复功能的时候,提示系统自动修复无法修复你的电脑,这是怎么一回事呢?那么有什么 ...

最新文章

  1. C语言 #ifndef 引起的redefinition of xxx 问题解决
  2. Oracle Hint 之 Parallel
  3. Kafka工作原理简要概述
  4. 在DataList控件中删除数据记录
  5. java简单投票系统_JSP实现的简单Web投票程序代码
  6. ListView(1)
  7. layui分页只能显示一页数据_分页显示查询数据
  8. java变量,初始化快,构造函数的执行顺序
  9. linux shell 子进程结束,关于linux:如何终止以shell = True启动的python子进程
  10. 基于ggplot2网络可视化(二)
  11. Linux (转)解析 xinetd.conf
  12. swift基础知识一
  13. Qt学习之C++基础
  14. 训练:Training Volume Landmarks for Muscle Growth - Renaissance Periodization论文阅读
  15. 基于SpringBoot生鲜商城
  16. 【NOIP2013模拟】比赛总结
  17. 【键盘开箱】B.FRIENDit壁虎忍者RF1430K 2.4G无线键盘,满足办公室安静需求的无线键盘!
  18. 服务器虚拟化专用ovf模板,Vmware虚拟机备份、OVF模板
  19. 起重机械 安全监控管理系统总结
  20. 小目标检测3_注意力机制_Self-Attention

热门文章

  1. 苹果2011年全球开发者大会图文实录
  2. 题目1(15分)对spark1.txt文件进行筛选,将A或者包含A的字母筛选出来并统计个数,然后输出到dome1文件中。
  3. C++题2:“五彩斑斓”
  4. 读懂algebraic distances on graphs
  5. 7000字用户画像技术及方法论
  6. 股票涨停板好不好?什么是股票涨停板和跌停板?
  7. 别再傻傻分不清:工业物联网关与PLC采集网关的区别了
  8. maven已支持支付宝开放平台SDK
  9. JVM垃圾回收中新生代老生代的问题
  10. airbnb_Airbnb对观众情感联系的误解导致双重抵制