脱糖魔法:为什么 java.time 在 Android 上还是会翻车? - Wesley's Blog
最近在写一个广告频控的功能,用 vibe coding 很快就开发完了,工具类引用了java.time.*,为了兼容旧安卓版本,需要启用脱糖,否则 IDE 会提示 api 的兼容性问题。项目的 AGP 版本是 8.0+,一开始运行没问题,但后面因为客户的工程是 AGP 4.0+,不愿意升级,所以要考虑兼容。由于在 agp 8.0+开发,要兼容好低版本,必须考虑第三方 SDK 的compileSdk、kotlin 和 AGP的版本要求,现在 maven 仓库居然不能直接查看 aar 的元数据,不能快速知晓这几个版本的要求,只能叫 AI 写个 gradle 版本 解析一下。坑都踩完后,发布 sdk 给客户,但客户 app 依赖sdk后,运行报错:java.Lang.NoSuchMethodError: No static method ofInstant(Lj$/time/Instant;Lj$/time/ZoneId;)Lj$/time/LocalDate;in class Lj$/time/LocalDate。究竟是为什么呢?分析之前先回顾一下语法糖和脱糖的知识点。
语法糖和脱糖
语法糖 = 让我们写代码有更爽的写法,本质上不增加能力,只是更加优雅地写代码。
比如:
lambdaforEach { }接口默认方法try-with-resources- Kotlin 的很多写法(
data class、when、?.let)等等
脱糖(desugar)= 把这些糖衣写法,改写成更基础、更原始的形式。
Android 里脱糖分两大类
A. 语言特性脱糖(Language Desugaring)
让 “新语法特性” 在旧 Android 版本上也能跑。
比如:lambda写法。
B. Java 标准库 API 脱糖(Core Library Desugaring / Library Desugaring)
旧 Android 系统里根本没有这些 API,或者缺很多方法,需要补上。
Android 的做法分为两步:
步骤 1:打包一份“补齐的库”进 APK(desugar_jdk_libs)
步骤 2:D8/R8 把调用点路由到这份库上 也就是把对 java.time.* 的调用改写到 j$.time.*(内部替身包名)
也就是打包 sdk 这一步是不会发生脱糖的,脱糖发生在 apk 编译。另外sourceCompatibility只能阻止新语法特性,不能阻止新 api 引入,这也能理解,因为新 api 是跟随安卓版本走的,虽然你用了 java 8 编写,但只要高版本系统支持,就可以引入。
对于 java 8 特性,需要 1.0+版本的库,对于 java 9+ 特性需要 2.0+的库。
android {
compileOptions {
// Flag to enable support for the new language APIs
// For AGP 4.1+
isCoreLibraryDesugaringEnabled = true
// For AGP 4.0
// coreLibraryDesugaringEnabled = true
// Sets Java compatibility to Java 8
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
// For AGP 7.4+
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// For AGP 7.3
// coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.3")
// For AGP 4.0 to 7.2
// coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.9")
}
问题分析
频控代码里面有这么一句,显示 java 9 引入。
LocalDate.ofInstant(Instant.ofEpochMilli(nowMillis), zoneId)
参考 通过脱糖获得 Java 8 及更高版本 API | Android Studio | Android Developers 里面可知,1.0 +的库是没有LocalDate.ofInstant 这个方法的,需要 2.0+的库才支持 java 9+ 特性: 通过脱糖提供的满足 Nio 规格要求的 Java 11 及更高版本 API | Android Studio | Android Developers。所以客户的 agp 4.0+,不能正确脱糖导致没有生成ofInstant方法,运行报错:java.Lang.NoSuchMethodError: No static method ofInstant(Lj$/time/Instant;Lj$/time/ZoneId;)Lj$/time/LocalDate;in class Lj$/time/LocalDate。
这里注意,虽然脱糖不成功,但是里面的LocalDate.ofInstant 已经路由到新包名了,所以下面的写法也是被禁止的,即使安卓 14 支持,但是由于脱糖,无论成不成功,都会路由到新包名,所以在安卓 14 也会崩溃。但是你直接 run debug 版本的时候,会根据你的安卓设备不进行脱糖处理,看起来好像没问题,所以必须单独编译 apk 进行验证。
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
LocalDate.ofInstant(Instant.ofEpochMilli(nowMillis), zoneId)
} else {
ZonedDateTime
.ofInstant(Instant.ofEpochMilli(nowMillis), zoneId)
.toLocalDate()
}
最后的修复方法是,按照 1.0 脱糖库的要求,不要使用 java 9+ api。
ZonedDateTime.ofInstant(Instant.ofEpochMilli(nowMillis), zoneId).toLocalDate()
参考
使用 Java 8 语言功能和 API | Android Studio | Android Developers
通过脱糖获得 Java 8 及更高版本 API | Android Studio | Android Developers
通过脱糖提供的满足最低规格要求的 Java 11 及更高版本 API | Android Studio | Android Developers
通过脱糖提供的满足 Nio 规格要求的 Java 11 及更高版本 API | Android Studio | Android Developers
Kotlin 版本所需的 AGP、D8 和 R8 版本 | Android Studio | Android Developers
工具和库的相互依存关系 | Android Studio | Android Developers
Android build 中的 Java 版本 | Android Studio | Android Developers