最近尝鲜了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崩溃,还原堆栈如下:
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:ui
和 androidx.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:ui
和 androidx.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味道浓重)