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

文章目录

    睡前看了一眼 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 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式