记一次Compose依赖导致的线上崩溃

最近尝鲜了Compose,将自己的一个项目的部分原生功能用Compose进行了重写,感叹Compose真是对原生开发方式的革命。

同时也发现Compose一些组件的灵活度不及原生(查看IssueTracker上Compose 组件相关的问题还真不少),也有可能是生态还不够丰富。

复刻阶段

好在经过大半个月时间将计划重写的功能写完了,不过也发现了用到组件的一些问题:

好在这些都可以暂时用原生来代替,等以后再替换。在最后 Release APK 上自测发现搜索功能的 TextField 在获取到焦点后进行手机截图会导致APP崩溃,还原堆栈如下:

log 复制代码
16:35:26.383 AndroidRuntime   E  FATAL EXCEPTION: main
                                  Process: com.dede.android_eggs, PID: 19312
                                  java.lang.NoSuchMethodError: No static method performImeAction$default(Lq1/SemanticsPropertyReceiver;Ljava/lang/String;Lp8/Function0;ILjava/lang/Object;)V in class Lq1/s; or its super classes (declaration of 'q1.s' appears in /data/app/~~7oHUIpRXTxq5UiBLJ9Fumg==/com.dede.android_eggs-ahuAm41sv_irUNiMr0eOjg==/base.apk)
                                         at androidx.compose.foundation.text.CoreTextFieldKt$CoreTextField$semanticsModifier$1.invoke(CoreTextField.kt:532)
                                         at androidx.compose.foundation.text.CoreTextFieldKt$CoreTextField$semanticsModifier$1.invoke(CoreTextField.kt:433)
                                         at androidx.compose.ui.semantics.CoreSemanticsModifierNode.applySemantics(SemanticsModifier.kt:73)
                                         at androidx.compose.ui.node.LayoutNode$collapsedSemantics$1.invoke(LayoutNode.kt:430)
                                         at androidx.compose.ui.node.LayoutNode$collapsedSemantics$1.invoke(LayoutNode.kt:421)
                                         at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2303)
                                         at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:496)
                                         at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:256)
                                         at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
                                         at androidx.compose.ui.node.OwnerSnapshotObserver.observeSemanticsReads$ui_release(OwnerSnapshotObserver.kt:121)
                                         at androidx.compose.ui.node.LayoutNode.getCollapsedSemantics$ui_release(LayoutNode.kt:421)
                                         at androidx.compose.ui.semantics.SemanticsNodeKt.SemanticsNode(SemanticsNode.kt:48)
                                         at androidx.compose.ui.semantics.SemanticsNode.fillOneLayerOfSemanticsWrappers(SemanticsNode.kt:268)
                                         at androidx.compose.ui.semantics.SemanticsNode.unmergedChildren$ui_release(SemanticsNode.kt:248)
                                         at androidx.compose.ui.semantics.SemanticsNode.getChildren(SemanticsNode.kt:327)
                                         at androidx.compose.ui.semantics.SemanticsNode.getReplacedChildren$ui_release(SemanticsNode.kt:298)
                                         at androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat_androidKt.getAllUncoveredSemanticsNodesToMap$findAllSemanticNodesRecursive(AndroidComposeViewAccessibilityDelegateCompat.android.kt:3635)
                                         at androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat_androidKt.getAllUncoveredSemanticsNodesToMap$findAllSemanticNodesRecursive(AndroidComposeViewAccessibilityDelegateCompat.android.kt:3637)
                                         at androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat_androidKt.getAllUncoveredSemanticsNodesToMap$findAllSemanticNodesRecursive(AndroidComposeViewAccessibilityDelegateCompat.android.kt:3637)
                                         at androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat_androidKt.getAllUncoveredSemanticsNodesToMap(AndroidComposeViewAccessibilityDelegateCompat.android.kt:3668)
                                         at androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.getCurrentSemanticsNodes$ui_release(AndroidComposeViewAccessibilityDelegateCompat.android.kt:344)
                                         at androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.canScroll-0AR0LA0$ui_release(AndroidComposeViewAccessibilityDelegateCompat.android.kt:439)
                                         at androidx.compose.ui.platform.AndroidComposeView.canScrollVertically(AndroidComposeView.android.kt:1639)
                                         at com.android.internal.view.ScrollCaptureInternal.detectScrollingType(ScrollCaptureInternal.java:78)
                                         at com.android.internal.view.ScrollCaptureInternal.requestCallback(ScrollCaptureInternal.java:170)
                                         at android.view.View.createScrollCaptureCallbackInternal(View.java:31999)
                                         at android.view.View.onScrollCaptureSearch(View.java:32053)
                                         at android.view.View.dispatchScrollCaptureSearch(View.java:32016)
                                         at android.view.ViewGroup.dispatchScrollCaptureSearch(ViewGroup.java:7667)
                                         at android.view.ViewGroup.dispatchScrollCaptureSearch(ViewGroup.java:7707)
                                         at android.view.ViewGroup.dispatchScrollCaptureSearch(ViewGroup.java:7707)
                                         at android.view.ViewGroup.dispatchScrollCaptureSearch(ViewGroup.java:7707)
                                         at android.view.ViewGroup.dispatchScrollCaptureSearch(ViewGroup.java:7707)
                                         at android.view.ViewGroup.dispatchScrollCaptureSearch(ViewGroup.java:7707)
                                         at android.view.ViewGroup.dispatchScrollCaptureSearch(ViewGroup.java:7707)
                                         at android.view.ViewRootImpl.handleScrollCaptureRequest(ViewRootImpl.java:10414)
                                         at android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl(ViewRootImpl.java:6295)
                                         at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:6092)
                                         at android.os.Handler.dispatchMessage(Handler.java:106)
                                         at android.os.Looper.loopOnce(Looper.java:205)
                                         at android.os.Looper.loop(Looper.java:294)
                                         at android.app.ActivityThread.main(ActivityThread.java:8416)
                                         at java.lang.reflect.Method.invoke(Native Method)
                                         at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
                                         at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)
16:35:26.384 ActivityManager   E  App crashed on incremental package com.dede.android_eggs which is 100% loaded.

当时感觉一脸懵逼,不过感觉搜索功能影响不大,而且这个问题出现的场景比较特殊就满心欢喜上线了🎉(没有BUG的代码不是好代码🐶)。

不过最近在 Github 上收到 Issues #135 关于这个问题的,而且复现流程更简单,TextInput 获取到焦点就崩溃,趁着元旦假期深入研究一下这个问题。

问题分析

通过堆栈 NoSuchMethodError: No static method performImeAction$default(Lq1/SemanticsPropertyReceiver;*) 不难看出是因为 Kotlin扩展方法SemanticsPropertyReceiver.performImeAction() 找不到原因导致的。我们就看看这个方法是什么: 可以看出是用于记录IME键盘动作的功能的,它刚好在 CoreTextField 组件内调用,负责输入框 语义属性相关功能的(这里和主题无关不过多介绍,可以查看文档),而 TextField 内部最终就调用了 CoreTextField

调用链如下:TextField -> BasicTextField -> CoreTextField --获取焦点--> SemanticsPropertyReceiver.performImeAction()

排查混淆

像方法找不到又是只有在Release包上才出现的,首先怀疑是不是混淆的问题,是不是什么原因将这个方法给混淆了或者删除了,但是现在复盘起来想这种可能根本站不住,因为它是在代码中显示引用了,不可能出现只有这个方法找不到的,但是当时有点"病急乱投医"了,配上混淆也要试试:

查看Kotlin Bytecode工具反编译的代码:

java 复制代码
// class androidx.compose.ui.semantics.SemanticsPropertiesKt
    public static final void performImeAction(@NotNull SemanticsPropertyReceiver $this$performImeAction, @Nullable String label, @Nullable Function0 action) {
      // ...
    }

   // $FF: synthetic method
   public static void performImeAction$default(SemanticsPropertyReceiver var0, String var1, Function0 var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = null;
      }

      performImeAction(var0, var1, var2);
   }

可以看出和崩溃堆栈的方法签名performImeAction$default(Lq1/SemanticsPropertyReceiver;Ljava/lang/String;Lp8/Function0;ILjava/lang/Object;)V刚好对应,那就加上对应的混淆配置

text 复制代码
-keep class androidx.compose.ui.semantics.SemanticsPropertiesKt {
    public static void performImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.lang.String, kotlin.jvm.functions.Function0, java.lang.Object);
    public static void performImeAction$default(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.lang.String, kotlin.jvm.functions.Function0, int, java.lang.Object);
}

大家肯定已经猜到结果了,问题照旧还是崩溃,那还能是什么原因导致这个问题呢

继续排查

最终在 IssueTracker #302680504 上也找到了别人反馈这个问题,其中有一层回答到:ref

perl 复制代码
The issue exists since `androidx.compose.ui` version `1.6.0-alpha01` and `google.accompanist` version  `0.33.0-alpha`.

google.accompanist这是谷歌推出的 Accompanist 库为了暂时补全 Compose 和 Android 原生一些特性交互的,例如:权限申请,Image 组件支持 Android Drawable 等。

项目中刚好也使用了 com.google.accompanist:accompanist-drawablepainter, 根据上面的回答说明只有在使用特定版本 com.google.accompanist:* 依赖上会出现问题,但是这是两个毫不相干的库,唯一的关系就是都使用了 Compose。等等都使用了 Compose,会不会是因为Compose依赖库版本导致的问题呢,就从这个方向切入

排查依赖

先检查出问题的代码所在的依赖:

Class 项目的依赖 真正的Android平台依赖
SemanticsPropertiesKt androidx.compose.ui:ui androidx.compose.ui:ui-android
CoreTextField.kt androidx.compose.foundation:foundation androidx.compose.foundation:foundation-android

刚好 Gradle 帮我们提供了查看项目依赖的工具,只要在命令行执行 dependencies task 即可打印所有依赖。 由于项目的配置众多,我们可以只打印某个场景下的依赖,既然问题出在Release APK上就打印releaseRuntimeClasspath 并输出到 output.txt

shell 复制代码
./gradlew app:dependencies --configuration releaseRuntimeClasspath > output.txt

仔细研究 task 输出还真找到了写端倪:

text 复制代码
+--- com.google.accompanist:accompanist-drawablepainter:0.33.2-alpha
|    +--- androidx.compose.ui:ui:1.6.0-alpha06 (*)
|    +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.7.3 (*)
|    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 (*)
...
+--- androidx.compose:compose-bom:2023.10.01
|    +--- androidx.compose.ui:ui:1.5.4 -> 1.6.0-alpha06 (c)
|    +--- androidx.compose.foundation:foundation:1.5.4 (c)
...

原本项目中的 androidx.compose.ui:uiandroidx.compose.foundation:foundation 的版本都受 Compose Bom 管理给配置在了1.5.4版本,由于依赖了com.google.accompanist:accompanist-drawablepainter:0.33.2-alpha,它内部依赖了androidx.compose.ui:ui:1.6.0-alpha06 导致了 androidx.compose.ui:ui 版本进行了升级,但是 androidx.compose.foundation:foundation 并没有升级。

是不是它两个库的版本不兼容导致的,检查一下 1.6.0-alpha06 版本的 SemanticsPropertiesKt 代码果然是不兼容的: 将原本的 performImeAction 方法 给改成了 onImeAction 并添加了新的参数 1.6.0-alpha06 版本的 CoreTextField 也是调用的 onImeAction 方法:

看来问题就是出现在这里:androidx.compose.ui:ui:1.6.0-alpha06 和 androidx.compose.foundation:foundation:1.5.4 不相互兼容导致的,那解决起来就简单了,排除 accompanist-drawablepainter 子依赖项即可

kotlin 复制代码
implementation("com.google.accompanist:accompanist-drawablepainter:0.33.2-alpha") {
   exclude(group = "androidx.compose.ui", module = "ui")
}

再次执行 app:dependencies task 发现已经生效了,各Compose依赖版本已经再次被 compose-bom 管理

text 复制代码
+--- com.google.accompanist:accompanist-drawablepainter:0.33.2-alpha
|    +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.7.3 (*)
|    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 (*)
...
+--- androidx.compose:compose-bom:2023.10.01
|    +--- androidx.compose.ui:ui:1.5.4 (c)
|    +--- androidx.compose.foundation:foundation:1.5.4 (c)
...

打包验证发现功能已经正常了

刨根问底

但是还有一个疑问为什么这个问题在Debug包上就不会出现呢,可以研究一下debug的classpath,去掉exclude运行task

shell 复制代码
./gradlew app:dependencies --configuration debugRuntimeClasspath > output.txt

对比 debug 和 release 两次输出

text 复制代码
+--- androidx.compose.ui:ui-tooling -> 1.6.0-alpha06
|    \--- androidx.compose.ui:ui-tooling-android:1.6.0-alpha06
|         +--- androidx.compose.ui:ui:1.6.0-alpha06 (c)
|         +--- androidx.compose.animation:animation:1.6.0-alpha06
|         |    \--- androidx.compose.animation:animation-android:1.6.0-alpha06
|         |         +--- androidx.annotation:annotation:1.1.0 -> 1.7.1 (*)
|         |         +--- androidx.compose.animation:animation-core:1.6.0-alpha06
|         |         |    \--- androidx.compose.animation:animation-core-android:1.6.0-alpha06
|         |         |         +--- androidx.annotation:annotation:1.1.0 -> 1.7.1 (*)
|         |         |         +--- androidx.compose.runtime:runtime:1.6.0-alpha06 (*)
|         |         |         +--- androidx.compose.ui:ui:1.6.0-alpha06 (*)
|         |         +--- androidx.compose.foundation:foundation-layout:1.6.0-alpha06
|         |         |    \--- androidx.compose.foundation:foundation-layout-android:1.6.0-alpha06
|         |         |         +--- androidx.compose.ui:ui:1.6.0-alpha06 (*)
|         |         |         \--- androidx.compose.foundation:foundation:1.6.0-alpha06 (c)
...
+--- com.google.accompanist:accompanist-drawablepainter:0.33.2-alpha
|    +--- androidx.compose.ui:ui:1.6.0-alpha06 (*)
|    +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.7.3 (*)
|    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 (*)
...
+--- androidx.compose:compose-bom:2023.10.01
|    +--- androidx.compose.ui:ui:1.5.4 -> 1.6.0-alpha06 (c)
|    +--- androidx.compose.ui:ui-tooling:1.5.4 -> 1.6.0-alpha06 (c)
|    +--- androidx.compose.foundation:foundation:1.5.4 -> 1.6.0-alpha06 (c)
...

发现多了个 androidx.compose.ui:ui-tooling:1.6.0-alpha06 依赖进而导致 androidx.compose.ui:uiandroidx.compose.foundation:foundation 全部都升级到了 1.6.0-alpha06 版本。而根源全部是因为debug模式下依赖了Compose实时预览工具依赖导致的

kotlin 复制代码
debugImplementation("androidx.compose.ui:ui-tooling")

但是还剩一个疑问,理论上 androidx.compose.ui:ui-tooling 也是受 Compose Bom 进行管理的,为什么 Compose Bom 没有将 androidx.compose.ui:ui-tooling 给配置在 1.5.4 版本而是使用了 1.6.0-alpha06 版本呢,这个有时间还要再深入研究一下

总结

像这种因为依赖版本覆盖升级的问题,平时开发中还是比较常见的,特别是在一些基础库的升级上,所以平时开发时一定要好好检查依赖变更,不要使用动态版本号,慎重使用非正式版本的依赖

虽然 Gradle 提供了不少依赖版本的解决方案,但是都需要人工进行排查。可以对 app:dependencies task 的输出文件进行分析和对比,同时集成到流水线中,作为上线前的一个检查,这里只是提供一个方案。

通过对Compose的尝鲜和排查线上的一个崩溃问题,了解了Compose的开发和开发Android的方式+1 。最近还在对 HarmonyOS ArkTS 进行学习,发现界面上和 Compose 写法有些类似,但是 ArkTS 还在预览阶段,还是不够成熟,而且不够强大,特别是对数据和状态的处理上非常薄弱。这个阶段想适配纯血鸿蒙难度属实比较大(ZZ味道浓重)

相关链接

相关推荐
mmsx17 分钟前
android sqlite 数据库简单封装示例(java)
android·java·数据库
m0_748247552 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203983 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2343 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
如若1234 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
吃着火锅x唱着歌4 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
滚雪球~5 小时前
npm error code ETIMEDOUT
前端·npm·node.js