脱糖魔法:为什么 java.time 在 Android 上还是会翻车?

0 阅读3分钟

脱糖魔法:为什么 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。究竟是为什么呢?分析之前先回顾一下语法糖和脱糖的知识点。

语法糖和脱糖

语法糖 = 让我们写代码有更爽的写法,本质上不增加能力,只是更加优雅地写代码。

比如:

  • lambda
  • forEach { }
  • 接口默认方法
  • try-with-resources
  • Kotlin 的很多写法(data classwhen?.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.*(内部替身包名)

使用“desugar”字节码转换实现 Java 8 语言功能支持

也就是打包 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