Android M之前View实例化时报访问成员变量空指针的问题

阅读: 评论:0

Android M之前View实例化时报访问成员变量空指针的问题

Android M之前View实例化时报访问成员变量空指针的问题

一、 背景

最近在新需求中,遇到一个crash问题,具体堆栈是:

android.view.InflateException: Binary XML file line #227: Error inflating class XXXViewat android.ateView(LayoutInflater.java:633)at android.ateViewFromTag(LayoutInflater.java:743)at android.view.LayoutInflater.rInflate(LayoutInflater.java:806)at android.view.LayoutInflater.inflate(LayoutInflater.java:504)at android.view.LayoutInflater.inflate(LayoutInflater.java:414)...
Caused by: flect.InvocationTargetExceptionat wInstance(Native Method)at wInstance(Constructor.java:288)at android.ateView(LayoutInflater.java:607)...
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.veCallbacks(java.lang.Runnable)' on a null object referenceat XXXView.cancelAnim(XXXView.kt:379)VisibilityChanged(XXXView.kt:103)at android.view.View.dispatchVisibilityChanged(View.java:8895)at android.view.ViewGroup.dispatchVisibilityChanged(ViewGroup.java:1178)at android.view.View.setFlags(View.java:9990)at android.view.View.<init>(View.java:4231)at android.view.ViewGroup.<init>(ViewGroup.java:529)at android.view.ViewGroup.<init>(ViewGroup.java:525)straintlayout.widget.ConstraintLayout.<init>(ConstraintLayout.java:580)at XXXView.<init>(XXXView.kt:32)at XXXView.<init>(XXXView.kt:31)at XXXView.<init>(XXXView.kt)...

该异常堆栈对应的业务伪代码为:

class XXXView @JvmOverloads constructor(context: Context,attr: AttributeSet? = null,defStyleAttr: Int = 0
) : ConstraintLayout(context, attr, defStyleAttr) {private val mainHandler = Handler()private val actionDismiss = Runnable {...}override fun onVisibilityChanged(changedView: View, visibility: Int) {VisibilityChanged(changedView, visibility)if (changedView != this) returnif (visibility == VISIBLE) {show()} else {cancelAnim()}}private fun cancelAnim() {veCallbacks(actionDismiss)...}
}

XXXView继承ConstraintLayout,在XXXView中,重写了onVisibilityChanged,当判断为隐藏时,调用cancelAnim方法取消动画,其具体实现会通过成员变量mainHandlerremoveCallbacks方法移除延时动画任务避免内存泄漏,该mainHandler对象在声明时实例化。

  • 开发调试手机为:OPPO Reno3 Pro,Android 10(Q),SDK 29。
  • 异常上报手机为:OPPO R9,Android 5.1(L_MR1),SDK 22。

二、 分析

从堆栈看,XXXView在实例化时,访问了XXXView的全局变量,此时变量还未实例化,导致NPE。

我们所知的一个对象实例化过程为:初始化成员变量->调用init函数->调用构造函数,这可以通过一个简单的demo证明:

class Demo {companion object {private const val TAG = "Demo"}private val obj = Any().apply {Log.i(TAG, "member variable")}constructor() {Log.i(TAG, "constructor")}init {Log.i(TAG, "init")}
}

Demo类实例化日志为:

按此顺序,理论上不存在构造方法中访问成员变量时还未初始化的问题。

接下来将从多个方向深入分析。


1. 指令重排序

指令重排序发生在编译期间,是编译器进行的优化操作,可能会将编译器认为不影响执行结果的代码进行顺序交换。例如:

val a = 1
val b = 2
val c = a + b

从逻辑上来看,a和b的赋值语句之间没有其他语句,这两行的顺序不会影响后续c的结果,那么编译时这两行可能会被置换顺序。

由于都是同一个kotlin文件的编译产物,不可能在运行时产生不同效果,因此排除了编译时指令重排序的可能。


2. 编写Demo复现问题

该问题的特殊性在于继承了父类,父类的构造方法间接调用了被子类重写的方法。

按照crash堆栈中的类关系和关键方法调用栈,编写一个简单的Demo,并补全日志:

实例化Child类时同样发生了crash,堆栈如下:

2021-04-19 15:17:17.586 u.demo I/Parent: member variable
2021-04-19 15:17:17.586 u.demo I/Parent: init
2021-04-19 15:17:17.586 u.demo I/Parent: constructor
2021-04-19 15:11:46.335 u.demo D/AndroidRuntime: Shutting down VM
2021-04-19 15:11:46.337 u.demo E/AndroidRuntime: FATAL EXCEPTION: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.String()' on a null object u.hod(Child.kt:16)u.demo.Parent.<init>(Parent.kt:10)u.demo.Child.<init>(Child.kt:10)u.Create(MainActivity.kt:10)at android.app.Activity.performCreate(Activity.java:7963)at android.app.Activity.performCreate(Activity.java:7952)...

这和本文的问题如出一辙。

查看Child类的字节码,其中类实例化函数<init>对应如下:

  // access flags 0x1public <init>()VL0LINENUMBER 15 L0ALOAD 0INVOKESPECIAL com/zengyu/demo/Parent.<init> ()VL1LINENUMBER 11 L1ALOAD 0NEW java/lang/ObjectDUPINVOKESPECIAL java/lang/Object.<init> ()VASTORE 1L2ICONST_0ISTORE 2L3ICONST_0ISTORE 3L4ALOAD 1ASTORE 4ASTORE 6L5ICONST_0ISTORE 5L6LINENUMBER 12 L6LDC "Child"LDC "member variable"INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)IPOPL7LINENUMBER 13 L7NOPL8GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;ASTORE 7ALOAD 6L9LINENUMBER 11 L9L10ALOAD 1L11PUTFIELD com/zengyu/demo/Child.obj : Ljava/lang/Object;L12LINENUMBER 19 L12NOPL13LINENUMBER 20 L13LDC "Child"LDC "init"INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)IPOPL14LINENUMBER 21 L14L15LINENUMBER 16 L15LDC "Child"LDC "constructor"INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)IPOPL16RETURNL17LOCALVARIABLE $this$apply Ljava/lang/Object; L5 L8 4LOCALVARIABLE $i$a$-apply-Child$obj$1 I L6 L8 5LOCALVARIABLE this Lcom/zengyu/demo/Child; L0 L17 0MAXSTACK = 3MAXLOCALS = 8

指令依次为:

  • 调用父类的实例化函数<init>
  • 初始化成员变量obj
  • 调用init函数
  • 调用构造方法constructor

而在第一步调用父类实例化函数时,通过被子类重写的方法,访问了子类还未初始化的成员变量,从而导致crash,这便是该问题的根本原因。


3. 对比SDK差异

在对比测试了其他安卓5.1的手机后,发现该crash为一个必现问题,而在高版本sdk的手机上,均未复现,且执行构造方法后未回调onVisibilityChanged

因此需要结合源码对比二者在调用栈上的差异。

在Android5.1上,View#setFlags中:

实例化View时便会去回调onVisibilityChanged

在Android6.0上,View#setFlags中:

回调onVisibilityChanged之前会判断mAttachInfo是否为空,而mAttachInfo赋值的时机是该View被添加到窗口,即绘制第一帧时,且赋值后会回调onAttachedToWindow,置空的时机是该View从窗口移除,且置空前会回调onDetachedFromWindow

可见在6.0,谷歌官方已经修复了这个可能导致开发者使用时崩溃的设计不合理的问题:不应该在构造方法中调用一个可被重写的方法。


三、 解决方案

和原生解决方案保持一致,在重写的方法中进行判断:

    override fun onVisibilityChanged(changedView: View, visibility: Int) {VisibilityChanged(changedView, visibility)if (!isAttachedToWindow) {return}...}

当View绘制第一帧之前,或从窗口移除之后,可见性变化的回调均会被忽略。

本文发布于:2024-01-31 04:54:23,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170664806625672.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:指针   变量   实例   时报   成员
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23