联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

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

作者:访客发布时间:2023-12-31分类:程序开发学习浏览:110


导读:最近尝鲜了Compose,将自己的一个项目的部分原生功能用Compose进行了重写,感叹Compose真是对原生开发方式的革命。同时也发现Compose一些组件的灵活度不及原生(...

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

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

复刻阶段

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

  • ModalBottomSheet 存在较多BUG,不满足线上使用的要求 ref: issuetracker.google.com/issues?q=Mo…
  • DropdownMenu 不支持背景圆角自定义 ref: issuetracker.google.com/issues/3030…
  • Tooltip 显示位置偏移量计算错误 ref: issuetracker.google.com/issues/1870…
  • ...

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

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工具反编译的代码:

// 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刚好对应,那就加上对应的混淆配置

-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

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平台依赖
SemanticsPropertiesKtandroidx.compose.ui:uiandroidx.compose.ui:ui-android
CoreTextField.ktandroidx.compose.foundation:foundationandroidx.compose.foundation:foundation-android

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

./gradlew app:dependencies --configuration releaseRuntimeClasspath > output.txt

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

+--- 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 子依赖项即可

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

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

+--- 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

./gradlew app:dependencies --configuration debugRuntimeClasspath > output.txt

对比 debug 和 release 两次输出

+--- 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实时预览工具依赖导致的

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味道浓重)

相关链接

  • Compose
  • Compose 物料清单
  • Accompanist
  • IssueTracker #302680504
  • 相关问题 Issues #135
  • 相关代码都存在项目 AndroidEasterEggs 中

标签:线上Compose


程序开发学习排行
最近发表
网站分类
标签列表