加载自定义 View 应用闪退 | @JvmOverloads 详解

前言

前不久项目中要求实现一些小控件,用来展示图表或者显示正反双方的人数对比,于是就自定义了一些 View,用于绘图

把 View 写好后,放到程序里跑起来,却闪退了,报错提示

1
2
3
4
5
6
7
java.lang.IllegalStateException: itemView.sev_safety_score_bar must not be null

或者

android.view.InflateException: Binary XML file line #77: Binary XML file line #77: Error inflating class
Caused by: android.view.InflateException: Binary XML file line #77: Error inflating class
Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]

原因分析

一般这种情况,会有几种原因:

  1. 引用类名问题:自定义一个 View 之后,将他用于布局文件中,必须使用完整路径名来引用,不能只使用类名,检查了一下,不是这个原因导致的

  2. 编译的中间文件没有清理干净,就是你在原生系统代码的编译环境下编译 APK 之后,特别是修改了 XML,出现标题所述现象,这个时候你只需要删除 out 目录下编译生成的中间文件夹即可(具体名字记不清了:在编译过程中,系统会将那个位置打印出来,通过串口来看吧,…/out/……/…./classes.dex,你循着这个路径往前推到你的应用的 project 名字那一层文件夹),删除再重新 make 就 OK 了,但是我好像也不是这个原因,Invalidate Caches / Restart 并不起作用

  3. 找不到 drawable 的资源文件,对应的 drawable 的 hdpi 或 xhdpi 等文件夹的图片资源有缺失,但是我显然不是因为这个

  4. 构造函数问题:自定义 View,派生实现基类 View 的构造函数不全

    1
    2
    3
    4
    5
    6
    7
    class SafetyExamView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
    }
    // 可以简化为以下这样
    class SafetyExamView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defStyle: Int = 0) : View(context, attributeSet, defStyle) {}

    从网上的一些说法来看,第二个和第三个构造函数对于XML这种引用方式是必须实现的,这三个构造函数是在不同的应用场合来实例化一个 View 对象的,但是事实是不是这样的呢,我发现了一些不同的看法

不过在当前这里看来,我在新建这个 View 编写构造函数的时候,有些构造函数的参数没写全,比如上面的第二条 constructor,后面我写成了: super(context),所以根本没有调用到父类的第二个构造函数,所以看来问题是出在这里了,当时看到提示后面的 Parameter ‘attrs’ is never used 还觉得无所谓,原来是不可以的,于是补上构造函数的参数,再次运行,果然没问题了

拓展

可以看到上面我使用了 @JvmOverloads 注解,那么刚好可以研究分析下这个 @JvmOverloads 注解是用来干嘛的,可以参考官方文档

指示 Kotlin 编译器为此函数生成替换默认参数值的重载。

如果一个方法有N个参数且其中M个具有默认值,则会生成M个重载:第一个参数采用 N - 1 个参数(除了最后一个参数采用默认值),第二个采用 N - 2 个参数,等等上。

更加详细的官方文档:

通常,如果你写一个有默认参数值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见,如果希望向 Java 调用者暴露多个重载,可以使用 @JvmOverloads 注解。

该注解也适用于构造函数、静态方法等。它不能用于抽象方法,包括在接口中定义的方法。

1
2
3
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) {
@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { …… }
}

对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。在上例中,会生成以下代码 :

1
2
3
4
5
6
7
// 构造函数:
Foo(int x, double y)
Foo(int x)
// 方法
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }

请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数。这就算没有 @JvmOverloads 注解也有效。

官方文档如是说,然而这次踩坑引出来的可不只是这个注解的问题,在我删除一些构造函数,复现报错然后把报错信息粘贴上来的时候,我记得好像并不是这个错误,这就奇怪了,难道还跟不同的构造方法有关?一查,果然,上文提到的报错信息里,当中的第一条,也就是:

1
java.lang.IllegalStateException: itemView.sev_safety_score_bar must not be null

是我在有 @JvmOverloads 注解的前提下,直接从: View(context, attributeSet, defStyle) 中把后两个参数删除,从而复现出来的,这就是说,重载三个构造函数的时候,调用的父类方法都是 View(context) 这个,这时就会报上面这条错误,说我的 View 为空

而第二条报错信息,也就是:

1
2
3
android.view.InflateException: Binary XML file line #77: Binary XML file line #77: Error inflating class 
Caused by: android.view.InflateException: Binary XML file line #77: Error inflating class
Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]

则是我直接把 @JvmOverloads 注解删除,也就是只声明了第三个构造函数时,所复现出来的,也就是说,在不写第一第二条构造函数的时候,就会找不到 XML 中声明的对应的类,而这时,我把 defStyle: Int 参数移除,就是变成第二种构造函数的时候,发现又可以正常运行了,而当我只写第一条构造函数时,又发生了这个报错,那就意味着,在缺少第二条构造函数时,就会报这个错误,而在存在第二条函数,但是函数参数不全时,就会报上一条错误

由此可以看出,第二个构造函数是必须的,在网上查阅了相关文章,发现确实是如此,得出以下结论:

  • Context:上下文,这个不用多说
  • AttributeSet attrs: 从xml中定义的参数
  • int defStyleAttr :主题中优先级最高的属性
  • int defStyleRes : 优先级次之的内置于View的style

在android中的属性可以在多个地方进行赋值,涉及到的优先级排序为:
xml 直接定义 > xml 中 style 引用 > defStyleAttr > defStyleRes > theme 直接定义

网上有很多关于三个构造函数使用时机的说法,搜到的比较正确的是:

  • 在代码中直接 new 一个 Custom View 实例的时候,会调用第一个构造函数,这个没有任何争议
  • 在 xml 布局文件中调用 Custom View 的时候,会调用第二个构造函数,这个也没有争议
  • 在 xml 布局文件中调用 Custom View,并且 Custom View 标签中还有自定义属性时,这里调用的还是第二个构造函数

也就是说,系统默认只会调用 Custom View 的前两个构造函数,至于第三个构造函数的调用,通常是我们自己在构造函数中主动调用的(例如,在第二个构造函数中调用第三个构造函数)

至于自定义属性的获取,通常是在构造函数中通过 obtainStyledAttributes 函数实现的

然后我自己做了个实验,验证了一些看法,也加深了自己的理解,首先上文中提及的 @JvmOverloads 注解,只是将 kotlin 的重载暴露给了 java 使用,也就是说,在 kotlin 中,可以直接利用构造函数赋默认值的方法来重载,但是因为 java 中没有这种机制,如官方文档所说:“写一个有默认参数值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见”,如果要将重载的方法暴露给 java,就要使用 @JvmOverloads 注解

而kotlin中的利用参数赋默认值来进行重载又是怎么样的呢,接下来可以看下我的实验:

  • 没有默认值,就没有重载,构造方法就是声明的那一条
  • 第一个参数有默认值
  • 第一第二个参数有默认值
  • 第一第三个参数有默认值
  • 第二第三个参数有默认值
  • 三个参数都有默认值

这里接近文档所说:“对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉”,但是好像应该作些修改,并不是把右边所有”参数“都移除,而是把右边所有”有默认值的参数移除“,这样似乎更加正确,不过还有待考证

参考文章:Android View 四个构造函数详解


加载自定义 View 应用闪退 | @JvmOverloads 详解
https://enderhoshi.github.io/2018/11/21/加载自定义 View 闪退和 JvmOverloads 注解/
作者
HoshIlIlI
发布于
2018年11月21日
许可协议