冰冷的 Android 空指针与暖心的 Google Play 韩国用户

更新日期: 2023-06-16 阅读次数: 709 字数: 936 分类: Android

睡前看了一眼 Google Play Console 里的 App 新版本发布情况,无意发现一条一小时前的应用评论。

是个韩国用户留的:

버그가 넘 심해요 ㅜㅜ 어플들어가면 자꾸 팅김요 ㅜㅜ 빠른시간에 수정부탁드립니다.유용하게 잘쓰고있는데 ㅜㅜ

翻译成中文是:

这个应用程序的错误太多了,每次进入都会崩溃。希望能尽快修复。我一直都在好好地使用它,但现在无法正常使用了。

上面这段翻译是用 Sage AI 翻译的,因为 Google 的翻译有问题,就是下面截图里的一小段英文(too bad),翻译成中文也是。

意外的是,在程序崩溃的情况下,用户依然给了五分好评。。。被暖到了

Google Play 韩国用户留言

我觉得崩溃的问题必须修复,大概率是老用户遇到的历史数据格式不兼容导致的。

在 Play Console 的 Android Vitals 崩溃和 ANR 中查看今天的崩溃日志,果然找到了两条,都是新版本相关。

java.lang.NullPointerException

Exception java.lang.NullPointerException:
  at com.sunzhongwei.shelflife.ui.home.HomeFragment.getBinding (HomeFragment.kt:24)
  at com.sunzhongwei.shelflife.ui.home.HomeFragment.access$getBinding (HomeFragment.kt:17)
  at com.sunzhongwei.shelflife.ui.home.HomeFragment$onViewCreated$1$1.emit (HomeFragment.kt:54)
  at com.sunzhongwei.shelflife.ui.home.HomeFragment$onViewCreated$1$1.emit (HomeFragment.kt:51)
  at app.cash.sqldelight.coroutines.FlowQuery$mapToList$$inlined$map$1$2.emit (Emitters.kt:223)
  at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke (SafeCollector.kt:15)
  at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke (SafeCollector.kt:15)
  at kotlinx.coroutines.flow.internal.SafeCollector.emit (SafeCollector.kt:87)
  at kotlinx.coroutines.flow.internal.SafeCollector.emit (SafeCollector.kt:66)
  at app.cash.sqldelight.coroutines.FlowQuery$asFlow$1.invokeSuspend (FlowExtensions.kt:48)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
  at android.os.Handler.handleCallback (Handler.java:978)
  at android.os.Handler.dispatchMessage (Handler.java:104)
  at android.os.Looper.loopOnce (Looper.java:238)
  at android.os.Looper.loop (Looper.java:357)
  at android.app.ActivityThread.main (ActivityThread.java:8090)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:548)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1026)

这个空指针大概率是 Flow 的读取操作没有执行完,就跳转到了其他 fragment,导致之前的 fragment 关联的 view 被销毁。 于是 getBinding 就出问题了。

临时解决方案是,判断一下 binding 是否为 null,然后再操作。

可能换成能自动处理生命周期的 LiveData 就不用操心这个事情了吧,怪不得大部分示例都推荐将 Flow 转换成 LiveData 再监听。 后续要实际测试一下。

lifecycleScope.launch(Dispatchers.Main) {
       homeViewModel.items.collect {
           adapter.submitList(it)
-          if (it.isNotEmpty()) {
-              binding.itemList.visibility = View.VISIBLE  // 崩溃发生在这行
-              binding.noItemMsg.visibility = View.GONE
-          } else {
-              binding.itemList.visibility = View.GONE
-              binding.noItemMsg.visibility = View.VISIBLE
+          if (binding != null) {
+              if (it.isNotEmpty()) {
+                  binding.itemList.visibility = View.VISIBLE
+                  binding.noItemMsg.visibility = View.GONE
+              } else {
+                  binding.itemList.visibility = View.GONE
+                  binding.noItemMsg.visibility = View.VISIBLE
+              }
           }
       }
   }

但是这个实现,还是存在隐患:

  1. binding 本身就是一个非空类型,而且用了双感叹号,判断为空没有意义,而且会导致崩溃
  2. 如果在 if 语句之后,binding 变成了 null,那么在访问 binding.attr 时还是会抛出空指针异常。为了避免这种情况,可以使用安全调用运算符来访问属性

合理的做法是:

lifecycleScope.launch(Dispatchers.Main) {
    homeViewModel.items.collect {
        adapter.submitList(it)
        if (it.isNotEmpty()) {
            _binding?.itemList?.visibility = View.VISIBLE
            _binding?.noItemMsg?.visibility = View.GONE
        } else {
            _binding?.itemList?.visibility = View.GONE
            _binding?.noItemMsg?.visibility = View.VISIBLE
        }
    }
}

java.lang.NumberFormatException

Exception java.lang.NumberFormatException:
  at java.lang.Integer.parseInt (Integer.java:747)
  at java.lang.Integer.parseInt (Integer.java:865)
  at com.sunzhongwei.shelflife.ui.edit.EditFragment$onViewCreated$2.invokeSuspend (EditFragment.kt:81)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
  at android.os.Handler.handleCallback (Handler.java:942)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loopOnce (Looper.java:226)
  at android.os.Looper.loop (Looper.java:313)
  at android.app.ActivityThread.main (ActivityThread.java:8757)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)

这个异常是因为字符串转换 int 类型报错了,应该是旧版本存储在 SQLite 中的是空字符串,而没有使用默认值 0 造成的。 将 toInt 替换成了 toIntOrNull,再判断一下就可以了。

-    initUnitAutoCompleteTextView(item.shelfLifeUnit.toInt())
+    val unit = item.shelfLifeUnit.toIntOrNull()
+    if (unit == null) {
+        initUnitAutoCompleteTextView(0)
+    } else {
+        initUnitAutoCompleteTextView(unit)
+    }

改完并发布新版本到 Google Play 已经 11 点多了,然后给这位热心用户回复了留言。

不得不说,我的 Android 开发经验还是少,对空指针的危险并没有认识的这么深刻,这次吃到经验了。 再就是大的版本更新,还是需要灰度发布,这次太冒险了。

睡觉。

关于作者 🌱

我是来自山东烟台的一名开发者,有敢兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式