睡前看了一眼 Google Play Console 里的 App 新版本发布情况,无意发现一条一小时前的应用评论。
是个韩国用户留的:
버그가 넘 심해요 ㅜㅜ 어플들어가면 자꾸 팅김요 ㅜㅜ 빠른시간에 수정부탁드립니다.유용하게 잘쓰고있는데 ㅜㅜ
翻译成中文是:
这个应用程序的错误太多了,每次进入都会崩溃。希望能尽快修复。我一直都在好好地使用它,但现在无法正常使用了。
上面这段翻译是用 Sage AI 翻译的,因为 Google 的翻译有问题,就是下面截图里的一小段英文(too bad),翻译成中文也是。
意外的是,在程序崩溃的情况下,用户依然给了五分好评。。。被暖到了
我觉得崩溃的问题必须修复,大概率是老用户遇到的历史数据格式不兼容导致的。
在 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
+ }
}
}
}
但是这个实现,还是存在隐患:
- binding 本身就是一个非空类型,而且用了双感叹号,判断为空没有意义,而且会导致崩溃
- 如果在 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 聊聊, 查看更多联系方式