加载自定义 View 应用闪退 | @JvmOverloads 详解
前言
前不久项目中要求实现一些小控件,用来展示图表或者显示正反双方的人数对比,于是就自定义了一些 View,用于绘图
把 View 写好后,放到程序里跑起来,却闪退了,报错提示
1 |
|
原因分析
一般这种情况,会有几种原因:
引用类名问题:自定义一个 View 之后,将他用于布局文件中,必须使用完整路径名来引用,不能只使用类名,检查了一下,不是这个原因导致的
编译的中间文件没有清理干净,就是你在原生系统代码的编译环境下编译 APK 之后,特别是修改了 XML,出现标题所述现象,这个时候你只需要删除 out 目录下编译生成的中间文件夹即可(具体名字记不清了:在编译过程中,系统会将那个位置打印出来,通过串口来看吧,…/out/……/…./classes.dex,你循着这个路径往前推到你的应用的 project 名字那一层文件夹),删除再重新 make 就 OK 了,但是我好像也不是这个原因,Invalidate Caches / Restart 并不起作用
找不到 drawable 的资源文件,对应的 drawable 的 hdpi 或 xhdpi 等文件夹的图片资源有缺失,但是我显然不是因为这个
构造函数问题:自定义 View,派生实现基类 View 的构造函数不全
1
2
3
4
5
6
7class 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 |
|
对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。在上例中,会生成以下代码 :
1 |
|
请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数。这就算没有 @JvmOverloads 注解也有效。
官方文档如是说,然而这次踩坑引出来的可不只是这个注解的问题,在我删除一些构造函数,复现报错然后把报错信息粘贴上来的时候,我记得好像并不是这个错误,这就奇怪了,难道还跟不同的构造方法有关?一查,果然,上文提到的报错信息里,当中的第一条,也就是:
1 |
|
是我在有 @JvmOverloads 注解的前提下,直接从: View(context, attributeSet, defStyle) 中把后两个参数删除,从而复现出来的,这就是说,重载三个构造函数的时候,调用的父类方法都是 View(context) 这个,这时就会报上面这条错误,说我的 View 为空
而第二条报错信息,也就是:
1 |
|
则是我直接把 @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中的利用参数赋默认值来进行重载又是怎么样的呢,接下来可以看下我的实验:
- 没有默认值,就没有重载,构造方法就是声明的那一条
- 第一个参数有默认值
- 第一第二个参数有默认值
- 第一第三个参数有默认值
- 第二第三个参数有默认值
- 三个参数都有默认值
这里接近文档所说:“对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉”,但是好像应该作些修改,并不是把右边所有”参数“都移除,而是把右边所有”有默认值的参数移除“,这样似乎更加正确,不过还有待考证