欢迎光临
我们一直在努力

附录 C:AOSP 不在公共框架 API 中采用 Kotlin 的原因

初次接触 AOSP 的读者很快会发现一种不对等现象:Kotlin 广泛应用于代码树上层模块,包括 SystemUI、Settings、Launcher3 以及部分 CTS;但应用编译所依赖的公共框架 API 依旧由 Java 定义。本附录阐述造成该现象的各类约束条件。本文并非倡导性内容,也不预测现状何时或者是否会发生改变。文中整理工程层面客观事实:Kotlin 允许使用的场景、禁止使用的场景、决定该差异的二进制契约,以及执行该契约的工具链。阅读完毕后,你可以查看frameworks/base/下任意类,判断该处 Kotlin 源码是无风险,还是会引发故障。

本附录最终推导出的规则十分简明:沿着类向外追溯到最近的 API 边界。如果该边界属于current.txt成员、@SystemApi、模块库导出项,或是其他应用、厂商代码所链接的内容,则适用冻结约束,此处引入 Kotlin 源码会带来 kotlinc 编译输出的风险。如果边界属于进程内部,会和框架同步重新编译(例如 LocalServices 接口、Binder 服务存根、SystemUI 内部类),则使用 Kotlin 是安全的。本附录通篇所指 “公共 API” 的精确定义,见《公共 API 契约》章节。

本文阅读顺序为从上至下,但各章节也可独立查阅。工程核心章节为《Java/Kotlin ABI 差异》与《工具链绑定约束》;其余章节介绍冻结约束、将约束扩展至厂商与 Mainline 接口,梳理平台内部已经安全使用 Kotlin 的位置。

关于资料来源说明:所有具体文件路径、代码行数、工具名称均来自直接查看 AOSP 代码仓库。文中提到约 65000 行、约 750000 行这类数值,是特定时间点的测量结果,会随着代码库迭代发生变动;论证只依赖数量级。

不对等现象

统计文件数量是观察该不对等现象最直观的方式。Kotlin 与 Java 源码在代码库中同时存在,但分布位置差异巨大。在 AOSP 仓库执行 find 命令,可以得到如下统计信息:

路径Kotlin 文件数说明
frameworks/base/packages/SystemUI/ 7846 重度使用者;系统 UI 外壳与快捷设置
packages/apps/Settings/ 1576 设置应用,应用层代码
cts/ 925 兼容性测试套件
packages/apps/Launcher3/ 949 桌面启动器应用
frameworks/base/services/ 237 全部位于权限子系统
frameworks/base/core/ 35 应用调用的 API 接口层
frameworks/base/(总计) 10461 全部子目录总和
frameworks/base/ Java 文件总计 17871 作为对比参考

表格中有两组关键数字支撑本附录论述。

第一组是 35。frameworks/base/core/存放构成 Android SDK 的android.*类。该目录仅有 35 个 Kotlin 文件,对比数以万计的 Java 文件,直接体现现有策略:公共 API 接口绝大多数由 Java 定义。

第二组是 237。frameworks/base/services/是system_server以及众多系统服务的所在目录。237 个 Kotlin 文件看起来使用规模可观,但查看位置可以发现:frameworks/base/services/下所有生产环境(非测试)Kotlin 文件全部位于权限访问子目录frameworks/base/services/permission/java/com/android/server/permission/access/。AMS、PMS、WMS、输入流水线、显示流水线这些核心服务依旧使用 Java。

本附录讨论的 “公共 API” 由 metalava 生成并校验的三份特征文件定义:

  • frameworks/base/core/api/current.txt — Android 公共 SDK 的标准特征文件,仓库内副本约 65000 行、4MB,采用 metalava 的 “6.0 特征格式”。
  • frameworks/base/services/api/current.txt — 暴露给进程内调用方的系统服务 API 接口。
  • frameworks/base/*/api/下对应的system‑current.txt、module‑lib‑current.txt,分别定义@SystemApi接口(平台签名组件可见)与模块库接口(Mainline 模块编译时可见)。
  • 这些特征文件是与编程语言无关的文本。文件本身不关心实现源码是 Java 还是 Kotlin;但本附录后续会说明,两份语言生成 JVM 特征的稳定性并不等同。

    阅读下文前第二个要点:该统计不代表frameworks/base/中 Kotlin 本身不安全;而是 Kotlin 不出现在公共 API 接口中。frameworks/base/core/存在 35 个 Kotlin 文件,frameworks/base/services/存在 237 个,这些文件可以编译、打包、运行。安全 / 不安全的边界划分在单个类级别,而非目录级别。frameworks/base/core/下的 Kotlin 类,只要不会出现在current.txt,就只是存放在 API 目录下的内部代码。判定依据是 metalava 是否收集该类,而不是源码存放路径。

    第三个要点:单向不对等。在依赖关系图中 SystemUI、Settings 处于框架 “下层”,它们消费公共 API,但不对外提供 API。它们可以自由使用 Kotlin,因为没有代码依赖其内部类结构。与之相反,框架处于上层,框架的类会被应用与厂商代码链接,因此必须保障二进制稳定性。同样的 Kotlin 代码在 SystemUI 中毫无风险,放到frameworks/base/core/java/android/就会产生风险。

    AOSP 各 API 接口层 Kotlin 使用权限

    实现代码是否允许 Java+Kotlin
    android.*类(SDK 生命周期冻结) 仅 Java
    @SystemApi类(跟随 Mainline 节奏冻结) 仅 Java
    模块间 API 仅 Java
    隐藏 API Java + Kotlin 均可
    服务、系统应用、CTS、支持库 Java + Kotlin 均可

    说明:该图不是构建依赖图,是语言使用权限图。上层模块强制使用 Java,避免下层使用的语言特性向外泄露。android.*的类内部可以调用 Kotlin 实现的服务,但调用必须通过 Binder 接口,或是 Java 签名的 Manager 类门面。一旦方法暴露给应用,就不能使用 Kotlin。

    公共 API 契约

    AOSP 语境中 “公共 API” 拥有精确定义:即frameworks/base/core/api/current.txt(以及配套system‑current.txt、module‑lib‑current.txt、test‑current.txt)内记录的类成员集合。该文件为可读文本,开头示例:

    // Signature format: 6.0
    package android {

    public final class Manifest {
    ctor public Manifest();

    每一条记录都是完整解析后的 JVM 特征:包名、访问修饰符、返回值、参数类型、异常。文件中不存在 Kotlin 关键字,该格式诞生早于 Kotlin,用于描述运行时所见接口,而非开发者编写的源码。

    每条记录格式细节:类声明嵌套在包块内;每个成员单独一行,包含:

    • 可见性(public、protected)
    • 固定顺序修饰符(static、final、abstract、synchronized、native、default)
    • 带包名限定的返回类型
    • 成员名称
    • 方法:带类型与名称的参数列表
    • 方法:可选 throws 子句,列出受检异常

    部分位置对空格缩进敏感(成员开头空格匹配嵌套层级),整体是工具生成产物。diff 工具可以清晰展示版本间差异;几乎所有框架变更都要执行m update‑api,产生文本差异交由 API 评审委员会逐条审核。

    系统并行维护多套接口特征:公共接口(current.txt)、@SystemApi(system‑current.txt)、Mainline 模块库接口(module‑lib‑current.txt)、测试 API(test‑current.txt),还有子系统独立文件如frameworks/base/services/api/current.txt。全部使用同一套格式。这些特征全部由 metalava 生成。metalava 本身是 Kotlin 编写的工具,位于tools/metalava/;通过 PSI 读取 Kotlin 源码,Turbine 读取 Java 源码,输出语言无关的特征文本。构建过程会重新运行 metalava,对比生成结果与仓库内current.txt,一旦出现不一致直接编译失败;新增 API 需要执行m update‑api,并将文本变更交由 API 委员会评审。

    框架类如何纳入公共 API 契约

    API 一旦随 SDK 正式发布,特征永久冻结。每个 SDK 版本将接口快照存入prebuilts/sdk/<N>/public/api/android.txt,同时锁定应用编译依赖的存根 jar 包prebuilts/sdk/<N>/public/android.jar。删除条目、修改参数 / 返回值、文本不变但底层 JVM 特征改动,全部属于破坏性变更。

    设备出厂时固化 SDK 版本,该版本对应 “永久稳定” 承诺。一台出厂 SDK30 的手机,四五年之后依旧运行 SDK30。以compileSdk=30编译的应用必须能够继续在该设备安装运行。厂商无法通过升级 kotlinc 编译器、更新元数据格式修复平台二进制契约的兼容性问题;兼容性由设备原始运行时类加载器决定。

    从首次发布到设备实际退役,周期大约十年,current.txt每一个成员都必须在此周期内保持可用。API 委员会审批新增方法时,都会考虑该时间窗口;API 一旦发布,签名不能撤回。

    Java/Kotlin ABI 差异

    兼容性问题不在于 Kotlin 能力缺失;而是 Kotlin 源码映射到 JVM 特征时,存在 Java 不存在的可变自由度。kotlinc 编译器、Kotlin 元数据格式、Kotlin 标准库都会迭代升级。编译器升级后,源码完全不变的类,其 JVM 对外特征有可能改变。

    对于内部代码,代码会跟随 kotlinc 同步重编译,该变化不会产生影响。但对于冻结的公共接口,任何变动都是二进制破坏性变更。

    本章节逐一介绍映射不稳定的具体特性场景。

    @JvmOverloads 与默认参数重载冻结

    Kotlin 允许函数声明参数默认值。如下源码:

    fun openFile(path: String, mode: Int = 0, encoding: String = "UTF-8"): File

    不加注解时,kotlinc 只会生成一条 JVM 方法:

    openFile(Ljava/lang/String;ILjava/lang/String;)Ljava/io/File;

    Java 调用方必须传入全部 3 个实参。添加@JvmOverloads注解,编译器自动生成多组重载,每一组对应末尾默认参数被截断:

    openFile(Ljava/lang/String;)Ljava/io/File;
    openFile(Ljava/lang/String;I)Ljava/io/File;
    openFile(Ljava/lang/String;ILjava/lang/String;)Ljava/io/File;

    放到公共接口会带来两类冻结风险:

  • 自动生成的重载集合取决于参数顺序;源码调换参数顺序,生成的 JVM 签名随之改变。
  • 默认值会以$default合成辅助函数(示例openFile$default)写入字节码。这些辅助函数属于 Java 调用方可见二进制接口的一部分。如果在公共类移除@JvmOverloads,会静默删除多条 JVM 方法,导致已编译调用程序崩溃。在末尾新增带默认值的参数会追加重载;但调换已有参数顺序会破坏已有符号。
  • 撰写附录时统计 AOSP 中@JvmOverloads仅存在于测试目录frameworks/base/services/tests/displayservicetests/下的测试工具类:TestUtils.kt、PersistentDataStoreTestUtils.kt、DisplayDeviceConfigTestUtils.kt、ClamperTestUtils.kt。生产服务完全不使用。原因很明确:AOSP 生产环境 Kotlin 代码只调用 Java 代码,不存在反向由 Java 调用 Kotlin 的场景,不需要自动生成重载。

    @JvmStatic 以及 Companion.foo () 与 Foo.foo () 的选择

    Kotlin 伴生对象的成员,Kotlin 内部看起来是静态调用MyClass.foo(),字节码层面会生成内部合成类Companion的实例方法。不添加@JvmStatic:

    class Foo {
    companion object {
    fun bar() = 42
    }
    }

    Java 侧可见签名:

    public final class Foo
    public static final class Foo$Companion
    public final int bar()
    public static final Foo$Companion Foo.Companion

    Java 代码必须写Foo.Companion.bar()。添加@JvmStatic注解,会在外层类额外生成一份静态副本:

    class Foo {
    companion object {
    @JvmStatic fun bar() = 42
    }
    }

    public final class Foo
    public static final int bar()
    public static final class Foo$Companion
    public final int bar()

    此时 Java 可以直接调用Foo.bar()。两种形式都会写入current.txt,一旦对外发布,两份方法都不能删除。

    AOSP 中@JvmStatic全部位于services/tests/测试子目录,用于 JUnit(Java)运行器调用 Kotlin 测试伴生对象上的@BeforeClass/@AfterClass。示例取自ApexUpdateTest.kt:

    @JvmStatic
    @BeforeClassWithInfo
    fun initApexHelper(testInformation: TestInformation) {
    apexInstallHelper = ApexInstallHelper(testInformation)
    }

    生产代码不使用@JvmStatic,平台没有 Java 代码去调用 Kotlin 静态方法。而公共 API 定义的类必须能够被 Java 应用调用;公共 Kotlin 类每一个静态工厂、常量都必须选定一套字节码输出形式,并且永久冻结。

    @JvmName 名称改写

    Kotlin 源码方法名可以和 JVM 层实际方法名不一致:

    @JvmName("safeOpen")
    fun open(path: String): File

    Kotlin 代码调用open(path);Java 代码调用safeOpen(path)。JVM 中只存在safeOpen。删除或者修改@JvmName参数,Java 链接使用的符号就会改名。放到公共接口,会直接破坏所有已经编译的应用。

    @JvmName还存在隐式生效场景:文件顶层函数编译为以文件名命名的合成类(例如Utils.kt生成UtilsKt);可以通过@file:JvmName("Utils")修改类名。框架公共接口契约没有办法处理这类改名,一旦改名,旧版本编译的应用直接失效。

    suspend 函数与 JVM 签名中的 Continuation

    源码 suspend 函数:

    suspend fun load(): Result

    编译输出 JVM 方法会追加kotlin.coroutines.Continuation参数:

    load(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

    返回值擦除为 Object,协程机制异步返回结果。Kotlin 调用方只看源码签名,编译器屏蔽转换细节;Java 调用方只能看到转换后的 JVM 签名:包含 Continuation 参数、Object 返回值、异常会被包装进kotlin.Result。

    带来两项稳定性问题:

  • kotlin.coroutines.Continuation属于 Kotlin 标准库类型,包名、方法、运行语义必须在 JVM 层面永久冻结。
  • suspend 转为 Continuation 是 kotlinc 编译器实现逻辑。历史 Kotlin 版本曾考虑过其他转换方案(状态机 vs CPS 风格、不同装箱策略)。未来 kotlinc 调整转换逻辑,同一源码会输出不同 JVM 签名。
  • Android 目前没有任何公共 API 暴露 suspend 函数。AndroidX 协程 API 独立于框架 SDK,以单独构件发布,版本迭代跟随 AndroidX 自身节奏。

    inline 函数向外暴露源码字节码

    Kotlin inline 函数不只是优化提示;契约要求函数体在每一处调用点做代码内联。常用于 reified 泛型参数,以及对性能敏感、接收 lambda 的 API。

    二进制稳定性隐患:inline 函数编译后的字节码(全部指令,包括私有辅助函数引用)会复制到每一个调用方 class 文件。框架发布公共 inline 函数,SDK 版本升级修改函数体,旧 SDK 编译出来的应用依旧携带旧版本内联代码。如果框架修复 inline 函数 Bug,只有重新编译的调用方才能拿到修复。

    面向数十亿已编译应用的平台,“修复仅对重新编译的调用者生效” 不可接受。Java 不存在该问题:static final方法实现可以替换,JVM 通过运行时类加载器解析。Kotlin inline 更接近 C++ 头文件模板,Java 方法的冻结规则对 inline Kotlin 函数不适用。

    Value Class / Inline Class 与参数签名改写

    Kotlin 值类(旧称 inline class)编译期包装单一底层值,调用边界解包:

    @JvmInline
    value class UserId(val value: Long)

    fun grantAccess(user: UserId)

    JVM 输出不会是grantAccess(LUserId;)V;kotlinc 会把参数特征哈希写入方法名,避免UserId与Long类型擦除后发生签名冲突:

    grantAccess‑{hash}(J)V

    哈希生成规则、名称分隔字符、合成构造函数行为,在不同 Kotlin 版本持续迭代。冻结的公共 API 不能容忍改写规则变化,也不能接受新增重载无意中改变已有方法哈希值。

    JVM 上 Result<T>名称改写

    kotlin.Result<T>本身就是值类。返回Result<Foo>的函数同样会触发名称改写:

    fun fetch(): Result<Data>

    JVM 层类似:fetch‑{hash}()Ljava/lang/Object;。Java 调用边界看到返回 Object,因为 Result 会发生类型擦除。Java 直接调用该 API 体验极差。

    Kotlin 官方文档不建议 Result 出现在供 Java 调用的公共 API。内部 Kotlin 互调可以使用 Result;面向公共对外接口不可行。

    伴生对象 INSTANCE 静态字段

    顶层 Kotlin 单例 object:

    object UriRegistry {
    fun lookup(scheme: String): Class<*>? = …
    }

    编译结果:

    public final class UriRegistry
    public static final UriRegistry INSTANCE
    public final Class<?> lookup(String)

    Java 调用写法:UriRegistry.INSTANCE.lookup("content")。INSTANCE字段属于二进制接口。Kotlin 源码修改 object 名字,类名随之改变;重构单例(例如改为按 userId 映射)会直接删除 INSTANCE 字段。两种改动对于已编译 Java 调用方都是破坏性变更,并且该字段会进入current.txt,必须在 SDK 整个生命周期保留。

    普通类companion object{}自动生成的Foo.Companion字段也有完全相同问题,属于二进制接口必须保留的公共字段。

    data class 合成与 componentN 访问器

    Kotlin 数据类,编译器自动生成一批成员:

    data class Point(val x: Int, val y: Int)

    JVM 大致生成如下内容:

    public final class Point
    public Point(int x, int y)
    public final int getX()
    public final int getY()
    public final int component1()
    public final int component2()
    public final Point copy(int x, int y)
    public static Point copy$default(Point, int, int, int, Object)
    public boolean equals(Object)
    public int hashCode()
    public String toString()

    全部合成成员属于二进制接口。componentN用于 Kotlin 解构语法,序号由源码字段顺序决定。调换源码字段顺序,component1与component2含义互换。copy 方法参数和构造函数保持一致;末尾新增字段会追加 copy 参数并保留copy$default;调换字段顺序会破坏使用命名参数 copy 的已编译调用方。

    公共 data class 需要在 SDK 整个生命周期永久锁定字段顺序、componentN 编号、copy 重载集合、equals/hashCode 实现语义。约束条件比 Java Record 更严苛,也高于普通手写 Java 类。

    顶层函数与 Kt 合成类

    Kotlin 允许在类外部直接定义顶层函数、属性。

    // File: PathUtils.kt
    package android.os

    fun normalizePath(path: String): String = …
    const val PATH_SEPARATOR = "/"

    kotlinc 编译生成文件名拼接 Kt 的合成类:

    public final class PathUtilsKt
    public static String normalizePath(String)
    public static final String PATH_SEPARATOR

    合成类名属于二进制接口。修改源码文件名,合成类名改变;可以通过@file:JvmName("PathUtils")修改后缀。Java 调用方使用PathUtilsKt.normalizePath(…);Kotlin 调用方看不到合成类,直接 import 函数。

    框架公共接口使用顶层函数,就必须永久绑定这个源码中不存在的合成类名。重命名源文件静默破坏 Java 编译产物;拆分源文件会拆分为多个合成类,同样引发故障。Java 没有对应问题,Java 静态方法全部放在开发者显式命名的类中。

    Boot 类路径共享:所有应用共用同一套标准库版本

    以上全部描述单个 Kotlin 源码如何产生整套 JVM 产物(签名、辅助函数、改写名称、元数据块),框架如果对外暴露 Kotlin API,就必须全部冻结。还有一层二进制稳定性风险:Android 运行时模型将框架公共 API 加载到所有应用进程共享的类加载器;公共 Kotlin API 会强制把kotlin‑stdlib.jar放入共享 Boot 类路径。

    框架公共 API 打包为设备上 Boot classpath 中的framework.jar,配套还有services.jar、framework‑graphics.jar、framework‑location.jar、ext.jar、telephony‑common.jar。由 Soong 通过PRODUCT_BOOT_JARS配置;默认集合定义在build/make/target/product/default_art_config.mk第 38 行:framework‑minus‑apex、ext、telephony‑common、framework‑graphics、framework‑location,以及各个 APEX jar 包(ART、conscrypt、i18n 等)。设备开机阶段,ART AOT 把这些 jar 编译为 boot 镜像,Zygote 进程加载到自身地址空间。

    com.android.internal.os.ZygoteInit.preloadClasses()(frameworks/base/core/java/com/android/internal/os/ZygoteInit.java第 284 行)读取/system/etc/preloaded‑classes,提前初始化列表中全部类;boot 镜像的类对象、静态字段、JIT 代码驻留 Zygote 堆内存。后续 fork 出的所有应用进程直接复用这些已经解析完毕的类对象;android.app.Activity在 Zygote 与每个应用中是完全相同类对象,不会每个应用重新加载。

    应用自身代码运行在下一级类加载器。安装的 APK 由dalvik.system.PathClassLoader加载,父加载器为 Boot 类加载器。ClassLoader.loadClass()遵循父优先委派逻辑:优先交给父加载器查找,失败才去自身 dex 查找。Boot classpath 中存在的类,优先级高于 APK 内部同名类。

    Java 场景该机制没有问题。框架传递依赖java.*、javax.*属于 OpenJDK 严格版本管控的核心库,具备 JLS 兼容性保障;就算应用想自带java.util.HashMap,类加载器也会优先使用平台版本。

    但对 Kotlin,这就是核心矛盾。公共接口出现 suspend 函数,就会引入kotlin.coroutines.Continuation;返回 Result<T>引入kotlin.Result;普通 Kotlin 类也会生成@kotlin.Metadata注解,Kotlin 反射在调用Foo::class读取。全部类型来自kotlin‑stdlib.jar。

    现状:AOSP 所有 Boot classpath jar 不会链接kotlin‑stdlib。external/kotlinc/Android.bp第 59 行把kotlin‑stdlib声明为预编译 jar 的 java_import;依赖它的模块不属于 Boot。SystemUI 插件与子模块显式设置static_kotlin_stdlib: false,保证 stdlib 打包进自身 APK,不会提升到全局共享状态。运行在 Boot jar 中的 Kotlin 代码(system_server 部分服务)对外边界不暴露任何 Kotlin 特有类型,因此不会把 stdlib 引用泄露到共享类加载器。

    一旦引入第一条公共 Kotlin 签名:暴露 Result 返回、suspend 参数、或是顶层函数合成 Kt 类带 Kotlin 元数据,对应的 framework.jar 就必须链接kotlin‑stdlib,并且 stdlib 会进入 Boot classpath。所有 Zygote fork 出来的应用进程解析kotlin.Result、kotlin.coroutines.Continuation、元数据类型都会使用 Boot 版本,不再使用 APK 内部自带版本。

    现实中应用 APK 经常打包不同版本 stdlib:同一个 APK 中库编译使用 Kotlin1.6,业务代码使用 Kotlin2.0;R8/D8 把所有内容压缩进 APK dex。父优先委派机制会强制使用设备 Boot 里的 stdlib,忽略应用 Gradle 选择版本。设备 stdlib 版本比应用旧,应用调用的方法缺失,运行抛出NoSuchMethodError;设备 stdlib 版本更新,空值 / 泛型签名收紧,应用编译调用点会字节码校验失败。应用开发者在 APK 内部无法规避,解析发生在自身类加载器上层。

    AOSP 现有规避同类冲突方案只有类加载器命名空间隔离。WebView 运行独立 Zygote 进程WebViewZygote.java,WebView APK 依赖不会污染主 Zygote 预加载集合。代价是额外 Zygote 进程,所有共享库双份副本,并且需要显式契约规定哪些类可以跨 Zygote 共享。如果要为使用 Kotlin 的应用照搬这套方案,要么按 stdlib 版本创建多个 Zygote(fork 时刻无法预知应用需要哪个版本),要么运行时改写类加载器逻辑,让每个应用看到自身 stdlib 同时解析android.*依旧走 Boot;这两套方案目前都不存在。

    Java 公共 API 与 Kotlin 公共 API 对比

    这张对比图是工程关键点。Java 一份源码映射为单一、确定 JVM 签名,javac 版本不改变映射规则。Kotlin 一份源码输出一整套 JVM 产物:签名、合成辅助、改写名称、Continuation 参数、value‑class 哈希,外加kotlin.Metadata元数据;输出结果取决于 kotlinc 版本、元数据版本、互操作注解。想要像 Java 一样冻结 Kotlin 公共 API,整套编译器输出行为都必须视作二进制契约,kotlinc 升级不能破坏已编译调用方。再结合上一节 Boot classpath,契约还延伸到设备共享类加载器强制所有应用使用的kotlin‑stdlib版本。

    工具链绑定约束

    《公共 API 契约》描述的签名契约由面向 Java 模型的工具链强制执行。部分工具本身使用 Kotlin 开发,但输入输出格式全部遵循 Java/JVM 签名模型。

    metalava 位于tools/metalava/,本身是 Kotlin 实现,子模块合计 657 个.kt 文件。metalava 用 Kotlin 开发,处理 Java 形态 API,这是约束条件,并不矛盾:metalava 可以读取 Kotlin 源码生成签名,但签名格式(定义在tools/metalava/FORMAT.md)没有任何 Kotlin 特有语法。current.txt无法表达suspend、inline、value class、data class。Kotlin 源码使用以上特性,要么被扁平化到 JVM 对外视图(丢失源码层面语义),要么被 API lint 拦截报错。

    扁平化逻辑:metalava 内部统一 Item 模型(类 Item、方法 Item、字段 Item),语言无关。PSI 前端读取 Kotlin 源码,Turbine 前端读取 Java 源码,输出同一套 Item 对象。metalava 输出签名时遍历 Item,按格式写入文本。Kotlin data class Foo (val x: Int) 经过 PSI 读取,投影为等价 Java 声明:包含 getX 访问器、合成构造函数、equals/hashCode/toString/copy/componentN 整套成员。签名文件保存投影结果,而不是原始 Kotlin 源码。永久冻结的契约是这份 JVM 投影;源码只是实现细节。

    这也说明:只要 Kotlin 成员不出现在对外接口,Kotlin 内部重构(data class 新增字段、密封类层级改动、顶层函数改名)不会改动current.txt。metalava 只收集 public/protected 并且没有@hide的成员。frameworks/base/core/那 35 个 Kotlin 文件绝大多数标记@hide或者包私有,API 检查会忽略它们。

    metalava 模块结构:

    • tools/metalava/metalava/ — 主工具入口
    • tools/metalava/metalava‑model/ — 与语言前端无关的抽象 API 模型
    • tools/metalava/metalava‑model‑psi/ — Kotlin 源码前端(JetBrains PSI)
    • tools/metalava/metalava‑model‑source/ — 通用源码模型
    • tools/metalava/metalava‑model‑text/ — 文本(签名文件)前端,用于 current.txt 读写回环
    • tools/metalava/metalava‑model‑turbine/ — Turbine Java 前端
    • tools/metalava/metalava‑reporter/ — 问题上报子系统
    • tools/metalava/metalava‑testing/ — 测试工具
    • tools/metalava/stub‑annotations/ — 生成存根使用的注解 jar

    文本模型是权威来源,PSI 与 Turbine 只作为输入。未来想要支持 Kotlin 公共 API,文本模型必须可以完整表达、无损回写所有 Kotlin 语言构造。

    文档生成流水线:平台参考文档同时运行 Doclava(传统 Javadoc 衍生)和 Dackka(新版支持 Kotlin)。Dackka 可以读懂 Kotlin 源码,但输出文档描述的是 Kotlin API 的 Java 投影,这才是应用开发者 IDE 实际看到的内容。Kotlin data class 文档会逐条列出合成的 equals、hashCode、toString、copy、componentN 方法,对应 Java 调用方所见。文档可以切换 Kotlin 视图展示源码形态,但底层契约依旧是 JVM 投影。

    隐藏 API 校验是工具链第二大支柱。黑名单维护在frameworks/base/boot/hiddenapi/下纯文本文件:

    • hiddenapi‑unsupported.txt — 完全拦截 API
    • hiddenapi‑unsupported‑packages.txt — 整个包接口拦截
    • hiddenapi‑max‑target‑o.txt — 适配 SDK O 及更早版本(旧版拦截)
    • hiddenapi‑max‑target‑p.txt — SDK P 及更早
    • hiddenapi‑max‑target‑q.txt — SDK Q 及更早
    • hiddenapi‑max‑target‑r‑loprio.txt — SDK R 及更早,低优先级

    被拦截的 Kotlin 扩展函数记录形式:Lcom/example/UtilsKt;‑>extensionMethod(Lcom/example/Receiver;)V,而不是 Kotlin 源码签名。每一行是标准 JVM 描述符格式,和 dexdump、ART 运行时检查、class 文件解析工具保持一致。Kotlin 编译输出 class 文件,因此 Kotlin 代码也通过这套描述符匹配;描述符记录的是 kotlinc 输出形态,不是 Kotlin 源码写法。

    构建系统将多个文本黑名单合并生成一份 CSV 产物:

    • out/soong/hiddenapi/hiddenapi‑flags.csv — 构建时产物,约 750000 行。每行包含成员描述符与标记列表(public‑api、sdk、system‑api、test‑api、blocked 等)。
    • prebuilts/runtime/appcompat/hiddenapi‑flags.csv — 随版本发布的预编译副本,用于应用兼容性检查,约 51MB。

    样例:

    Landroid/Manifest$permission;‑><init>()V,public‑api,sdk,system‑api,test‑api
    Landroid/Manifest$permission;‑>ACCEPT_HANDOVER:Ljava/lang/String;,public‑api,sdk,system‑api,test‑api
    Landroid/Manifest$permission;‑>ACCESSIBILITY_MOTION_EVENT_OBSERVING:Ljava/lang/String;,blocked,test‑api

    ART 运行时加载该 CSV。应用调用 API 时运行时检查标记;blocked 抛出异常;max‑target 系列标记触发警告或者按版本拦截。粒度为描述符。一个 Kotlin 源码声明产生多条 JVM 描述符(@JvmOverloads生成多重载、Companion + @JvmStatic 两套方法、value‑class 改写名称),CSV 就要维护多条记录表达同一个源码 API 意图。缺少上层映射,开发者维护 Kotlin 公共 API 很难确认全部生成描述符是否分配正确标记。

    jarjar 规则:部分框架模块构建阶段改写依赖类名,避免与应用可见类冲突。规则写在.jarjar文件,由 jarjar 工具处理。Kotlin 的@kotlin.Metadata元数据注解内部字符串记录原始类名。jarjar 把kotlin.collections.MapsKt改名为com.android.internal.kotlin.collections.MapsKt会和元数据内容不匹配,运行时 Kotlin 反射故障。Java 没有嵌入元数据,jarjar 对 Java 只是简单文本替换。

    @SystemApi、@UnsupportedAppUsage由 metalava 和隐藏 API 工具链共同处理。@SystemApi扩大平台签名调用方可见接口,输出到system‑current.txt冻结签名。@UnsupportedAppUsage标记成员写入 hidden‑api CSV 并附带 max‑target 标记。Kotlin 要支持这套注解,既需要 Kotlin 源码注解,也需要 metalava 规则把注解信息正确投射到 CSV。

    注解本身存在细微约束:@SystemApi接收客户端类型参数(MODULE_LIBRARIES、PRIVILEGED_APPS、MODULE_APPS),限定下游哪些调用方可以访问该成员。metalava 读取参数分流到对应接口特征文件;注解配置错误会泄露到错误的current.txt,m checkapi检测该泄露。Java 源码处理注解逻辑明确:注解写在声明上,metalava 读取 AST 节点完成分流。Kotlin 源码场景 metalava 依靠 PSI 前端解析;未来新增 Kotlin 注解特性(文件注解、扩展目标、复杂保留策略重复注解)都需要在tools/metalava/…/ExtractAnnotations.kt显式增加解析逻辑。

    out/soong/hiddenapi/hiddenapi‑flags.csv是所有输入的汇总点:黑名单 txt、@SystemApi、@UnsupportedAppUsage、公共 API 存根描述符,全部经由 Soong 汇入这份以描述符为键的表格。该产物同时生成预编译版本prebuilts/runtime/appcompat/hiddenapi‑flags.csv,旧版本运行时做应用兼容降级逻辑使用。描述符格式、标记集合、Kotlin 成员映射逻辑一旦改动,整条流水线都受影响。

    OEM、厂商分区与 Mainline 约束

    公共 API 契约并不是系统唯一冻结点。厂商分区、Mainline 模块两套接口,同样需要长期稳定。

    厂商分区冻结

    OEM 设备出厂 SDK 版本 N,厂商分区基于 SDK N 冻结的 system‑api、module‑lib 接口编译。同一设备后续系统升级(Treble 框架‑厂商分离)必须保证对原有厂商分区二进制兼容。system‑api 作为框架(内部允许 Java+Kotlin,对外 Java 形态)与厂商代码(多为 C++,部分 Java)之间二进制契约。如果@SystemApi类因为 Kotlin 编译器升级改变输出特征,会破坏所有使用该厂商分区的设备。

    Mainline APEX 模块冻结

    Mainline 模块是 APEX 包,存放平台组件,通过应用商店更新,而不是完整 OTA 升级。架构文档:system/apex/docs/README.md。每个 Mainline 模块声明min_sdk_version,基于该 SDK 版本的 module‑lib 接口编译。APEX 构建规则位于build/soong/apex/apex.go与build/soong/android/apex.go,强制 APEX 不能依赖低于自身 min_sdk_version 的符号。

    应用商店下发的 Mainline 模块,要能够在发布多年的旧设备运行,解析全部引用符号必须匹配设备上冻结的 module‑lib 接口。SDK N 到 N+3 之间框架新增 Kotlin 形态公共方法,SDK N 设备不存在该符号。Mainline 模块只能二选一:提高min_sdk_version(放弃旧设备覆盖);继续使用 Java 形态接口(无损失)。

    kotlinc 发布节奏与 AOSP 节奏

    Soong 对 kotlinc 版本做硬锁定。预编译编译器放在external/kotlinc/,版本记录在external/kotlinc/build.txt,例如2.2.0‑release‑294。

    Soong 编译器参数相关代码:

    • build/soong/java/kotlin.go — kotlinc 调用 Ninja 规则、kotlin jar 快照、增量编译
    • build/soong/java/kotlin_test.go — kotlinc 规则单元测试
    • build/soong/java/config/kotlin.go — kotlinc 二进制路径、插件、禁止覆盖的编译标记(‑no‑jdk、‑no‑stdlib、‑language‑version)。平台统一决定 Kotlin 语言版本,单个模块不能升级 / 降级。kotlinc JVM 启动参数‑J‑Xmx8192M应对一次性编译大量 Kotlin 模块内存压力。

    锁定编译器版本带来耦合:AOSP 选定版本,全代码库验证,对外发布。公共 API 使用该 kotlinc 输出 JVM 签名。升级 kotlinc(为 Compose 新特性、内部语言特性、安全补丁)有可能改变公共 Kotlin 类输出签名。当前规避风险的方案:公共类全部使用 Java,风险直接消除。

    内部 Kotlin(服务、SystemUI、Settings、各类应用)不受影响;kotlinc 升级后全部内部代码重新编译。冻结产物公共存根与 hidden‑api CSV 文件会伴随 kotlinc 升级重新生成,变更通过 API 与 hidden‑api 校验后合入。

    举例说明版本节奏风险:假设android.os.SomeClass新增一条 Kotlin 公共方法,SDK N 版本,kotlinc 2.2.0 编译,prebuilts/sdk/N/public/api/android.txt记录该版本输出签名。设备出厂 SDK N,存根 jar 固化该签名。一年后 AOSP 升级 kotlinc 至 2.4.0 启用新 Compose 能力。若同样源码,kotlinc2.4.0 输出不同 JVM 签名(改写哈希等),SDK N 编译的应用在新版框架设备上符号解析失败。框架要么永久保留旧编译器输出行为(升级失去意义),要么编写兼容转发层,接口集合膨胀。Java 不存在该问题,javac 版本不改变输出签名。

    厂商分区侧问题:厂商分区在设备出厂时编译一次,设备生命周期不再重编。厂商服务链接框架 Kotlin API,会固化 kotlinc‑N 输出特征。系统 OTA 升级框架到 kotlinc N+1,厂商分区依旧期望旧输出。框架无法重新编译厂商分区。Java 完全规避该风险;公共接口引入 Kotlin 会新增一条冻结维度 “kotlinc 编译输出形态”,现有厂商分区契约没有处理该维度。

    Mainline 场景矛盾更加突出:Mainline 模块更新频率高于系统 OS。一个 Mainline APEX 设定min_sdk_version = Android11,需要运行在所有 SDK11 以上设备。SDK12 框架新增 Kotlin 公共 API,Mainline 模块想要使用,可选路径:

  • 提升min_sdk_version到 12,放弃旧设备。
  • 运行时反射条件调用 API,丧失编译期类型检查。
  • 使用等价 Java API。
  • 现实选择是方案 3。Mainline 模块内部可以写 Kotlin,但对外入口保持 Java 形态。

    历史背景:Treble 项目正式定义框架‑厂商分离,system‑api 成为跨分区稳定契约。Mainline 定义框架‑模块分离,新增 module‑lib 接口。每一条新的冻结维度,都配套签名管理、工具链、测试基础设施。新增 “Kotlin 编译输出形态” 作为第四条冻结维度,需要同等配套工作,目前尚未落地。

    APEX 格式与更新流程更多参考仓库文档。

    AOSP 中现有 Kotlin 使用位置

    Kotlin 在平台中已经有相当规模,但使用范围局部化。承接《不对等现象》统计表,补充各路径作用:

    路径Kotlin 文件数量作用说明
    frameworks/base/packages/SystemUI/ 7846 系统 UI 外壳:锁屏、通知、快捷设置、状态栏、系统栏、Compose UI
    packages/apps/Settings/ 1576 设置应用
    packages/apps/Launcher3/ 949 桌面与最近任务界面
    cts/ 925 兼容性测试套件;测试代码自由使用 Kotlin
    frameworks/base/services/ 237 全部在 services/permission 子目录,外加测试代码;其余系统服务生产代码无 Kotlin
    frameworks/base/core/ 35 框架内部少量工具,不暴露公共 API
    tools/metalava/ 657 签名工具本身由 Kotlin 编写

    frameworks/base/services/下生产 Kotlin 集中在 Android13 引入的权限访问子系统。选取三份代表性源码说明代码形态。

    AccessCheckingService.kt

    路径:frameworks/base/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt,323 行。新版权限栈入口类。继承 SystemService,向 LocalServices 注册 Manager 接口,通过getState{…}作用域助手暴露内部状态。关键片段:

    @Keep
    class AccessCheckingService(context: Context) : SystemService(context) {
    @Volatile private lateinit var state: AccessState
    private val stateLock = Any()

    override fun onStart() {
    appOpService = AppOpService(this)
    permissionService = PermissionService(this)

    LocalServices.addService(AppOpsCheckingServiceInterface::class.java, appOpService)
    LocalServices.addService(PermissionManagerServiceInterface::class.java, permissionService)

    该类可以安全使用 Kotlin 的关键点:对外暴露接口AppOpsCheckingServiceInterface、PermissionManagerServiceInterface全部是 Java 接口,注册进 Java 形态服务注册表。其他系统代码调用走 Java 接口,永远看不到 Kotlin 实现类。Binder 接口定义在IPermissionManager.aidl,AIDL 编译生成 Java 存根。

    内部实现大量地道 Kotlin 写法。文件底部:

    @OptIn(ExperimentalContracts::class)
    internal inline fun <T> getState(action: GetStateScope.() -> T): T {
    contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
    return GetStateScope(state).action()
    }

    该声明用到三个放到公共接口会产生风险的 Kotlin 特性:带接收者函数类型GetStateScope.()‑>T(无 Java 等价);inline 函数搭配类似 reified lambda,代码会内联进每一个调用方字节码;实验性 contract API,仅源码层面生效。但该函数标记 internal,包路径仅服务端可见,调用方全部是同包其他 Kotlin 类。全部风险特性在此处完全安全,边界限制在单一子系统内部 Kotlin 互调。

    如果getState放到公共 Java 接口,inline 必须改为普通方法(丧失 inline 收益);带接收者函数类型改为显式参数;contract 没有等价表达。最终代码可读性、性能都会下降。这也是该团队选择内部 Kotlin 实现、对外边界使用 Java 的原因。

    AccessPolicy.kt

    路径:frameworks/base/services/permission/java/com/android/server/permission/access/AccessPolicy.kt,527 行。AccessPolicy 维护 SchemePolicy 实现映射,将各个 scheme 业务逻辑委派给子类。关键定义:

    class AccessPolicy
    private constructor(
    private val schemePolicies: IndexedMap<String, IndexedMap<String, SchemePolicy>>
    )

    后续文件内定义抽象基类SchemePolicy。抽象类与子类AppIdPermissionPolicy、DevicePermissionPolicy、AppIdAppOpPolicy、PackageAppOpPolicy、AppIdAppFunctionAccessPolicy全部包私有,不会出现在任何签名文件。

    Permission.kt

    路径:frameworks/base/services/permission/java/com/android/server/permission/access/permission/Permission.kt,185 行。data class 代表单条权限记录,附带伴生对象常量。

    data class Permission(
    val permissionInfo: PermissionInfo,
    val isReconciled: Boolean,
    val type: Int,
    val appId: Int,
    @Suppress("ArrayInDataClass") val gids: IntArray = EmptyArray.INT,
    val areGidsPerUser: Boolean = false
    ) {

    companion object {
    const val TYPE_MANIFEST = 0
    const val TYPE_DYNAMIC = 2

    fun typeToString(type: Int): String = …
    }
    }

    该文件演示多个公共 API 会出问题的特性:data class 自动生成 equals、hashCode、toString、copy、componentN;参数默认值;伴生对象常量。但外部services/permission/之外没有代码直接引用 Permission 类型,因此没有风险。

    权限子系统测试代码

    services/tests/目录 Kotlin 测试代码补充说明:JUnit 运行器是 Java。Kotlin 测试想要@BeforeClass,Java 运行器需要测试类上静态方法。Kotlin 测试写在 companion object,添加@JvmStatic。services/tests/displayservicetests/测试工具使用@JvmOverloads把带默认参数的方法暴露给尚未迁移 Kotlin 的 Java 测试代码。这类注解只用于测试;生产 Kotlin 只调用 Java,不需要反向暴露给 Java。

    其他位置补充说明

  • CTS Kotlin 测试:约 925 个 Kotlin 文件。CTS 验证 OEM 设备兼容性;测试运行器是 Java,但测试主体可以写 Kotlin。CTS 不会随系统镜像发布,只是运行在设备之上,不受永久冻结约束。
  • Settings、Launcher3 应用层:随系统镜像打包,但本质是普通应用。编译使用公共 SDK,生命周期和第三方应用一致;内部大量 Kotlin。Settings 与框架之间 ABI 边界依旧是公共 API+System‑API,保持 Java 形态。
  • SystemUI:靠近system_server进程,实现锁屏、通知、快捷设置、系统栏;AOSP 最大 Kotlin 代码库 7846 文件。SystemUI 和平台其余部分边界清晰:Binder 调用走 AIDL,共享状态走 ContentProvider,跳转 Activity 使用 Intent。边界不传递任何 Kotlin 类型。SystemUI 内部可以自由重构;对外 ABI 由 AIDL 与 Intent 契约约束。
  • frameworks/base/services/测试代码:和前面测试场景一致,@JvmStatic、@JvmOverloads仅测试使用。
  • frameworks/base/core/中 Kotlin:共 35 个文件,工具类、辅助类、少量新代码。没有任何类成为current.txt公共条目;没有android.*命名空间 public 类型被 metalava 收集为公开成员。只要不越界公共 API 就可以存放 Kotlin 源码;API 检查工具强制执行该边界。
  • kotlin‑stdlib 在 Boot 镜像:Soong 计算 Boot classpath 时,stdlib jar 会参与编译 Boot 镜像内 Kotlin 类,解析依赖。因此kotlin.collections.MapsKt、kotlin.coroutines.Continuation、kotlin.Result、kotlin.Metadata运行时对系统全部进程可见。metalava 会排除这些类型,不会写入current.txt。未来公共 API 支持 Kotlin,就需要决定 stdlib 类型是否算作公共 API(只要返回 stdlib 类型就会传递暴露),或是公共 API 只允许经过审核的 stdlib 子集。
  • 公共接口最棘手的 Kotlin 语言特性

    前面 ABI 差异章节讲解编译输出机制,本章节从设计压力角度归纳各类特性对永久冻结公共接口带来的约束。

    伴生对象

    参见《@JvmStatic》小节。伴生对象必须选定是否在外层类暴露静态方法;该选择写入current.txt,发布后不可撤销。内部代码没有 Java 调用方,默认行为完全没问题。公共类该选择永久生效,影响所有应用开发者 IDE 体验。附带副作用:标记@JvmStatic的成员字节码存在两份,一份在 Companion 内部类,一份在外层类。反射会看到两套副本;遍历类层次的工具(Hilt 依赖注入、Mock 生成器、注解扫描)都要处理重复。

    默认参数

    参见《@JvmOverloads》小节。永久冻结约束:公共 Kotlin 函数调换参数顺序,静默破坏已生成重载;后期新增 / 删除@JvmOverloads改变重载集合。演化风险:即使纯 Kotlin 调用场景,函数末尾新增带默认值参数,源码层面兼容,但二进制不一定兼容。因为$default合成辅助函数使用位掩码,掩码宽度取决于参数数量。一旦参数数量越过 kotlinc 内部阈值,$default形态改变,所有调用方必须重新编译。Java 没有该问题,只能显式新增重载。

    inline / value class

    参见 value‑class 章节。改写哈希规则由 kotlinc 决定;接收 / 返回 value class 每一个方法 JVM 签名包含哈希值,不是稳定字符串。公共 API 必须把整套改写算法作为二进制契约,编译器后续不能修改。次要风险:value class 在某些调用边界装箱、某些边界拆箱;何时装箱何时拆箱属于 kotlinc 输出行为,直接影响 Java 调用方看到的 JVM 签名。

    typealias(类型别名)

    类型别名仅源码层面生效。typealias UserId = Long编译为 Long,签名无变化。内部 Kotlin 完全安全;公共边界场景 metalava 需要在输出current.txt前展开别名。metalava 当前已经实现该逻辑。风险点完全来自工具链。副作用:typealias 不会在 API 中保留独立身份;两个别名底层同一类型,JVM 无法区分,current.txt只能记录解析后底层类型。对于命名属于契约一部分的公共 API,别名只是源码便利,API 边界必须抹平。

    suspend 函数

    参见 suspend 章节。Continuation 参数、Object 返回值,是 kotlinc 协程转换实现。冻结公共 API 就必须冻结该转换逻辑。suspend 同时绑定协程运行库:kotlin.coroutines提供 Continuation;真正调度、上下文、取消、结构化并发实现在独立库kotlinx.coroutines,版本节奏独立,不在 Boot classpath。公共 suspend API 需要明确隐式依赖哪一套协程库,或是自带一套最小运行库,或是完全不绑定;全部都不是简单决策。

    空值注解

    Kotlin T? / T 在 JVM 层面体现为@Nullable/@NonNull注解(JetBrains 注解包)。字节码方法签名本身不受注解影响。Kotlin 调用 Java API 依靠注解推断可空性;框架 Java 公共 API 使用androidx.annotation.Nullable/androidx.annotation.NonNull。源码迁移 Kotlin,要么继续显式保留 AndroidX 空值注解,要么依靠 kotlinc 输出 JetBrains 注解;框架必须确定对外契约使用哪一套注解命名空间。平台类型也会带来隐患:Kotlin 读取不带空注解 Java API,得到平台类型,编译期不做空检查;反过来 Kotlin 公共 API 被 Java 调用,空值信息会丢失,除非 metalava 把空值信息转成 Java 可见注解。

    sealed class /sealed interface(密封类 / 接口)

    Kotlin 密封类限制子类只能在同一文件 / 模块。字节码打上kotlin.Metadata标记,供 Kotlin exhaustive when 校验。Java 语言层面看不到密封限制,JVM 不会阻止 Java 继承该类。kotlinc 目标 JVM17 + 才输出 JVM 原生PermittedSubclasses属性。公共 Kotlin 密封类,强制执行效果取决于编译目标版本,多一层冻结维度。必须选定一套密封语义,在 SDK 全生命周期兼容 Kotlin、Java 调用方。

    扩展函数

    示例fun String.lastSegment(): String。编译为静态方法,第一个参数接收者对象;静态方法所在类来自源文件名(默认UtilsKt)。Java 调用方当作合成类静态方法;Kotlin 调用方使用接收者点语法。框架新增公共扩展函数会在 Kt 合成类新增静态方法;删除就移除该方法。源文件存放位置、接收者参数位置全部记录进current.txt。

    共性总结:以上特性内部使用非常便利,对外公共接口处处受限。根源:Kotlin 源码抽象能力比 Java 丰富;代价是一份源码编译输出多份 JVM 产物,具体输出内容取决于编译器。永久冻结接口,每一份产物都必须可命名、可索引、每一次编译器升级都完整保留。

    落地需要满足哪些条件

    本部分仅列出当前已知约束,并非正式路线图。如果 AOSP 项目决定在公共框架 API 层支持 Kotlin,必须完成下面这些前置工作,条目按依赖顺序排列:后一项依赖前一项全部就绪。

  • 稳定的 Kotlin 元数据格式,作为二进制契约对外声明 每个 Kotlin 类文件内嵌kotlin.Metadata注解,编码源码层面信息:密封类继承关系、可空性、默认参数、suspend 函数脱糖逻辑,供 Kotlin 反射与各类工具读取。当前该格式跟随 kotlinc 编译器版本变化。Kotlin 社区已经在 KEEP 提案中讨论过二进制稳定性问题。
  • 若 AOSP 要在公共 API 层面使用 Kotlin,元数据格式必须作为对外独立版本化的二进制契约,明确向前、向后兼容保障,并且废弃策略要匹配 AOSP 长达十年的兼容生命周期。 现有kotlin.Metadata版本兼容规则:kotlinc N可以读取有限 N‑K 版本之前编译器产出的元数据。这种兼容范围对于频繁重编译的 Gradle Kotlin 项目够用,但无法支撑十年级别的系统版本维护。

  • 改造 Metalava,使其支持 Kotlin 并输出等价于 current.txt 的签名文件tools/metalava/FORMAT.md定义了签名格式,需要新增语法来描述 Kotlin 特有构造:suspend、内联函数、值类、数据类、密封类 / 接口、默认参数、空安全、伴生对象结构。 每一项扩展本身都是 API 设计难题:新增语法必须可以双向序列化读写,还要能兼容未来 kotlinc 编译器迭代。 需要扩展tools/metalava/metalava-model-text/文本模型模块,完成 Kotlin 新语法的解析与输出;同时修改ComparisonVisitor.kt对比逻辑,定义哪些 Kotlin 变更属于破坏性 API 变更。 目前 Metalava 可通过 PSI 模型读取 Kotlin 源码,但最终只输出 Java 投影形式的 API 签名。
  • 隐藏 API 校验机制支持识别 Kotlin 描述符out/soong/hiddenapi/hiddenapi‑flags.csv使用原始 JVM 描述符。Kotlin 类虽然已经可以出现在该文件中(由 kotlinc 编译生成),但一条源码注解如@JvmOverloads会生成多条 JVM 描述符记录,很难基于源码做统一策略管控。 基于描述符的 CSV 文件,需要配套一层高层映射:源码声明 X 属于公共 API,则对应的一组 JVM 描述符{d1, d2,…dN}必须保持完全一致的隐藏 API 标记。 缺少该映射,开发者修改 Kotlin 公共 API 时,无法简单确认编译出来的全部 JVM 描述符都被划分到正确的隐藏 API 分类。
  • 文档工具链升级 Dackka 现已支持读取 Kotlin 源码,但参考文档需要一套统一模型,同时把Kotlin 源码视图与Java JVM 投影视图作为一等公民对待。 使用 Java 开发工具调用 Kotlin 平台 API 的应用开发者,需要看到连贯的 Javadoc;使用 Kotlin 工具链的开发者要看到原生 Kotlin 签名。现有模型底层假定 API 是 Java 形态。 真正双语 API 表面,就要配套双语文档。举例:Kotlin 数据类,Kotlin 侧展示原生字段,Java 侧展示编译器合成的componentN()组件方法。
  • API 委员会敲定命名规范规则tools/metalava/API‑LINT.md中的 Lint 规则全部面向 Java 编码习惯:is/get 访问器成对、集合复数命名、setOnXxxListener回调注册模式。 Kotlin 特有写法:属性语法、运算符重载、中缀函数、扩展函数,需要 API 委员会明确接纳 / 拒绝策略并形成规范。 规则还要兼顾 Java 可调用投影:公共类上 Kotlin 的var会编译出getX/setXJava 访问器,规范必须明确契约基准:以 Kotlin 源码为准、以 Java 访问器名为准,还是两者同时约束。
  • kotlinc 发布节奏对齐 AOSP 迭代周期 框架所选用的 kotlinc 版本,不能对任意公共类产生可感知的签名变动。这是约束最强的一条,因为它绑定两个独立组织的发布周期。 AOSP 大约每年发布一次大版本 SDK;kotlinc 迭代更快,版本边界和 SDK 不匹配。 可行方案:每个公共 API 快照冻结一套 kotlinc 版本,类似LOCAL_SDK_VERSION机制,每一个对外发布的 SDK 版本绑定生成签名文件时所用的 kotlinc 编译器。 需要 Soong 增加机制,追踪每一份current.txt由哪个 kotlinc 编译生成,并对下游 Mainline 模块强制该版本绑定。
  • 迁移与审计工具 即便以上全部就绪,AOSP 仍要承担一次性迁移成本。每一个计划改为 Kotlin 实现的 Java 公共 API 类,都需要并行审计:确认经过支持 Kotlin 的新版 Metalava 处理后,输出的current.txt条目和原来 Java 源码输出完全一致;一旦条目出现差异,就代表发生二进制 ABI 破坏。目前还不存在这套审计工具。
  • suspend 异步 API 对应的协程运行时方案决策 文档《公共 API 最难兼容的 Kotlin 特性》提到:对外暴露suspendAPI,会强制调用方依赖协程运行时。AOSP 三选一: (a) 将kotlinx.coroutines作为平台库冻结,承担全套二进制稳定性要求; (b) AOSP 自研一套最小协程运行时; (c) 在公共 API 层面完全不使用suspend。 任意选择都代表多年长期维护投入。
  • 以上清单只是 AOSP 源码树内可见约束快照,不是对未来实现的预测,也不代表推动落地的提议。


    动手实践

    下面 5 个实操练习,可以在本地 AOSP 源码树验证本附录的观点,全部命令在 AOSP 根目录执行。

    练习 C‑1:统计平台源码树 Kotlin 文件数量

    附录开头不对称统计表,就是统计指定路径下.kt文件得到。在本地执行同样 find 命令,对比附录统计表。

    cd $AOSP
    echo "frameworks/base/services Kotlin: $(find frameworks/base/services -name '*.kt' | wc -l)"
    echo "frameworks/base/core Kotlin: $(find frameworks/base/core -name '*.kt' | wc -l)"
    echo "packages/SystemUI Kotlin: $(find frameworks/base/packages/SystemUI -name '*.kt' | wc -l)"
    echo "packages/apps/Settings Kotlin: $(find packages/apps/Settings -name '*.kt' | wc -l)"
    echo "packages/apps/Launcher3 Kotlin: $(find packages/apps/Launcher3 -name '*.kt' | wc -l)"
    echo "cts Kotlin: $(find cts -name '*.kt' | wc -l)"
    echo "frameworks/base total Kotlin: $(find frameworks/base -name '*.kt' | wc -l)"
    echo "frameworks/base total Java: $(find frameworks/base -name '*.java' | wc -l)"

    预期输出:数量量级和表格接近;frameworks/base/core、frameworks/base/servicesKotlin 文件数量远小于 Java 总数。精确数字会随源码迭代不断变化。

    练习 C‑2:查看公共 API 签名文件

    打开frameworks/base/core/api/current.txt,该文件约 65000 行,建议使用分页工具查看。

    cd $AOSP
    # 查看文件头部
    head -5 frameworks/base/core/api/current.txt

    # 查找Manifest类定义
    grep -n 'public final class Manifest ' frameworks/base/core/api/current.txt

    # 定位该类Java源码
    find frameworks/base/core -name 'Manifest.java' -path '*/java/android/*'

    # 确认是Java源码
    head -3 frameworks/base/core/java/android/Manifest.java

    观察点:签名文件头部标记// Signature format: 6.0,随后package android {,全部类使用 Java 语法描述;Manifest.java真实存在,是 Java 文件,不是 Kotlin。

    练习 C‑3:追踪 Kotlin 实现的 Binder 跨进程 Service

    AccessCheckingService是平台为数不多生产环境 Kotlin 实现的 Service。虽然使用 Kotlin 编写,但对外给其他系统代码的契约仍然是 Java 形态。

    cd $AOSP
    # Kotlin实现源码
    ls -l frameworks/base/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt

    # 确认继承SystemService
    grep -n 'class AccessCheckingService' \\
    frameworks/base/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt

    # 查询向LocalServices注册的Java接口
    grep -rn 'PermissionManagerServiceInterface\\|AppOpsCheckingServiceInterface' \\
    frameworks/base/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt

    # 查找Binder接口AIDL定义
    find frameworks/base -name 'IPermissionManager.aidl'

    观察点:AccessCheckingService继承 Java 基类SystemService,注册 Java 接口;Binder 接口定义在 AIDL 文件,编译生成 Java 桩代码。Kotlin 实现逻辑不会以 Kotlin 形态跨进程边界传递。

    练习 C‑4:在 AOSP 源码查找 @JvmStatic / @JvmOverloads

    这两个注解用来让 Kotlin 代码可以被 Java 调用。框架生产代码很少使用它们:大多是 Kotlin 调用 Java,反向调用场景不多。在源码树搜索验证该现象。

    cd $AOSP
    # services目录查找@JvmStatic
    grep -rln '@JvmStatic' frameworks/base/services/ | head -10

    # services目录查找@JvmOverloads
    grep -rln '@JvmOverloads' frameworks/base/services/ | head -10

    # 仅搜索生产代码,排除test测试目录
    grep -rln '@JvmStatic' frameworks/base/services/ | grep -v '/tests/' | head -10
    grep -rln '@JvmOverloads' frameworks/base/services/ | grep -v '/tests/' | head -10

    观察点:前两条搜索命中全部在tests/测试目录;过滤测试目录之后,生产代码无命中。

    核心结论:AOSP Service 中的 Kotlin 是单向 Kotlin 调用 Java,不需要对外暴露供 Java 调用;因此生产代码看不到@JvmStatic、@JvmOverloads。一旦做公共 Kotlin API,就要大规模使用这两类注解,并且永久固化它编译输出的形态。

    练习 C‑5:研读 Metalava 工具

    Metalava 是定义公共 API 契约的核心工具,阅读它目录结构、格式规范、兼容性文档。

    cd $AOSP
    # 顶层目录结构
    ls tools/metalava/

    # 阅读README前40行
    head -40 tools/metalava/README.md

    # current.txt格式文档
    wc -l tools/metalava/FORMAT.md
    head -40 tools/metalava/FORMAT.md

    # API兼容性策略
    head -40 tools/metalava/COMPATIBILITY.md

    # API Lint规则
    head -40 tools/metalava/API-LINT.md

    # metalava子模块数量
    ls -d tools/metalava/metalava-* | wc -l

    # 工具自身Kotlin源码文件总数
    find tools/metalava -name '*.kt' | wc -l

    观察点:Metalava 本身大量使用 Kotlin(约 657 个 kt 文件),但它输出签名格式FORMAT.md完全没有 Kotlin 特有语法;COMPATIBILITY.md管控current.txt变更是否合法,没有独立 Kotlin 兼容分支;各个metalava‑*子模块分别对应不同语言前端、文本模型。本练习不需要实际运行 metalava 二进制程序。


    总结

    AOSP 内部大量使用 Kotlin,但公共 API 表面看不到 Kotlin,这不是编码风格偏好,而是由 4 个约束共同决定,全部围绕 SDK 冻结签名快照产物:prebuilts/sdk/<N>/public/api/android.txt以及源码frameworks/base/core/api/current.txt。

  • 契约生命周期约束 设备发布后会锁定某个 SDK 版本长达接近十年。current.txt每一条签名,必须兼容未来所有 kotlinc 版本、Kotlin 元数据格式变更、标准库改动。内部 Kotlin 不受该限制:编译器签名变化可以通过下一次全量重编译抹平。
  • 二进制 ABI 映射差异 Java 源码声明与 JVM 签名一一对应。Kotlin 一条源码会产出一组 JVM 产物:重载方法、名字混淆、伴生对象访问器、合成辅助函数、Continuation 参数、元数据块;组合结果完全依赖编译器。要冻结这套集合,每一部分都要独立锁定。文档《Java‑Kotlin ABI 鸿沟》列举 8 大类会出现该多映射现象,每一类都带来冻结难题。
  • 工具链约束 Metalava、隐藏 API 校验、Doclava/Dackka、jarjar,还有@SystemApi/@UnsupportedAppUsage注解链路全部基于 JVM 描述符工作。虽然 Metalava、Dackka 可以读取 Kotlin 源码,但输出都是与语言无关的 JVM 契约:current.txt文本、CSV 描述符、Javadoc 网页。 要产出 Kotlin 形态公共 API,整套工具链需要并行新增一套原生 Kotlin 构造建模能力。现有工具并非排斥 Kotlin,只是还没有建模 Kotlin 源码层语义。
  • 运行时类库共享约束 Framework jar 包加载到 Boot 类路径,所有从 Zygote fork 出来的 App 进程全部共享 Boot 类路径。双亲委派类加载机制:BootClasspath 里面的类型优先级高于 APK 内部同名类。 公共 API 暴露 Kotlin 签名,就必须把kotlin‑stdlib放入 BootClasspath。这会覆盖 App Gradle 打包自带的 kotlin‑stdlib 版本。设备厂商无法在镜像内部修复,App 开发者也无法在 APK 规避;唯一隔离方案类似 WebView 独立 Zygote 进程隔离,AOSP 目前仅在 WebView 场景付出这套沉重代价。
  • 统计结果印证现状:Kotlin 大量用于应用 UI 层、测试代码、Metalava 本身、权限子系统,少量存在于frameworks/base/core。但是不会出现在current.txt公共 API,因为完整落地成本至今没有全部完成。

    关键源码路径速查表

    表格

    路径用途
    tools/metalava 签名工具;工具本身约 657 个 Kotlin 文件,输出语言无关签名文本
    tools/metalava/FORMAT.md current.txt文本格式规范
    tools/metalava/COMPATIBILITY.md Metalava 执行的 API 签名变更兼容策略
    tools/metalava/API‑LINT.md API Lint 检查规则文档
    frameworks/base/core/api/current.txt android.* 公共 API 快照,约 65000 行
    frameworks/base/services/api/current.txt 系统服务 API 集合
    frameworks/base/api/ 签名生成构建逻辑:api.go、Android.bp、StubLibraries.bp、ApiDocs.bp
    prebuilts/sdk/<N>/public/api/android.txt SDK N 版本冻结公共 API 快照
    prebuilts/sdk/<N>/public/android.jar App 编译使用的 SDK 存根 Jar 包
    out/soong/hiddenapi/hiddenapi‑flags.csv 编译生成隐藏 API 描述符表,约 75 万行
    build/soong/java/kotlin.go Soong kotlinc Ninja 编译规则:编译、快照、增量编译
    external/kotlinc/ 预编译锁定版本 kotlinc 工具链
    frameworks/base/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt 生产 Kotlin Service 样例,继承 SystemService
    libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java App 类加载器,父加载器为 Boot 类加载器
    frameworks/base/core/java/android/webkit/WebViewZygote.java WebView 独立 Zygote,实现类加载隔离
    赞(0)
    未经允许不得转载:171主机测评 » 附录 C:AOSP 不在公共框架 API 中采用 Kotlin 的原因
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址