跳转到内容
Skip
3.2k

内部架构

本文档描述 Skip 的内部架构:Swift 源代码如何变成一个运行中的 Android 应用。它涵盖构建插件集成、skipstone 处理流水线、Gradle 项目生成、资源处理和应用启动序列。有关 Skip 两种模式的高级概述,请参见 Lite 与 Fuse 模式

Skip 作为 SwiftPM 构建插件运行,集成到 Xcode 的构建系统中。当你构建项目时,Skip 处理依赖树中的每个 Swift 模块,生成一个包含 Kotlin 源代码的并行 Gradle 模块树。然后 Gradle 编译并将 Kotlin 打包为 Android 应用,与 iOS 一起在模拟器或设备上启动。

flowchart LR
    A["Swift Source\n(SwiftPM Modules)"] --> B["Skip Build Plugin\n(skipstone)"]
    B --> C["Kotlin Source\n(Gradle Modules)"]
    C --> D["Gradle Build\n(Android SDK)"]
    D --> E["Android App\n(.apk)"]
    style B fill:#4A90D9,color:#fff

过程因每个模块的模式而异:

  • Skip Lite 模块的 Swift 源代码由 Skip 转译器转译为 Kotlin。
  • Skip Fuse 模块的 Swift 源代码使用 Swift SDK for Android 为 Android 原生编译,并自动生成到 Kotlin 的 JNI 桥接。

两种模式都生成参与同一 Android 构建的 Gradle 模块。

Skip 的构建基础设施跨越两个仓库:

仓库提供
skipstoneskip 二进制命令行工具
skipskipstone SwiftPM 构建插件和测试框架

skip 仓库提供一个名为 skipstone 的 SwiftPM 构建工具插件。在发布构建中,此插件从 skipstone 仓库下载预构建的 skip 二进制文件。对于本地开发(当设置了 SKIPLOCAL 或工作目录以“skipstone”结尾时),它使用本地构建的版本。

skipstone 插件实现了 SwiftPM 的 BuildToolPlugin 协议。在构建过程中,Xcode 为项目中的每个目标调用 createBuildCommands(context:target:)。插件执行以下操作:

  1. 扫描目标中的 Skip/skip.yml 配置文件。没有此文件的目标将被排除。
  2. 解析依赖:遍历目标的依赖图,收集同样具有 skip.yml 文件的对等模块。
  3. 创建符号链接:链接到依赖模块的插件输出目录,以便每个模块在 Gradle 编译期间可以引用其依赖。
  4. 发出构建命令:使用适当参数调用 skip CLI。
sequenceDiagram
    participant Xcode
    participant Plugin as skipstone Plugin
    participant CLI as skip CLI
    participant Gradle

    Xcode->>Plugin: createBuildCommands(target)
    Plugin->>Plugin: Scan for Skip/skip.yml
    Plugin->>Plugin: Resolve module dependencies
    Plugin->>Plugin: Create dependency symlinks
    Plugin->>CLI: Invoke Skip CLI
    CLI->>CLI: Transpile Swift → Kotlin (Lite)
    CLI->>CLI: Bridge Swift → Kotlin (Fuse)
    CLI->>CLI: Generate build.gradle.kts
    CLI->>Gradle: Kotlin source + Gradle config
    Gradle->>Gradle: Compile Kotlin + package APK

插件将输出写入标准 SwiftPM 插件输出目录。确切路径因环境而异:

环境输出路径
XcodeDerivedData/.../SourcePackages/plugins/<package>.output/<target>/skipstone/
SwiftPM 5 CLI.build/plugins/outputs/<package>/<target>/skipstone/
SwiftPM 6 CLI.build/plugins/outputs/<package>/<target>/destination/skipstone/

在每个模块的输出目录中,Skip 生成一个完整的 Gradle 模块:

  • 文件夹ModuleName/
    • build.gradle.kts 从 Package.swift + skip.yml 生成
    • 文件夹src/
      • 文件夹main/
        • 文件夹kotlin/
          • 文件夹module/
            • 文件夹name/ Kotlin 包(如 skip.ui)
              • TranspiledFile.kt 每个 .swift 源文件对应一个 .kt
        • 文件夹assets/ 已处理资源(Android AssetManager)
        • 文件夹res/ Android 资源(字符串、值)
      • 文件夹test/
        • 文件夹kotlin/ 转译的 XCTest 用例(JUnit)
    • .sourcehash 增量构建标记

Skip 使用 .sourcehash 标记文件跟踪源文件修改。每个模块的构建命令将其 .sourcehash 文件声明为输入(供依赖模块使用)和输出。这创建了一个依赖链,确保模块按正确顺序转译,并且仅在源代码发生变化时才进行。

每个模块的 Skip/skip.yml 文件控制 Skip 如何处理该模块。此文件示例:

skip:
mode: 'native' # Fuse 用 'native',Lite 省略或 'transpiled'
bridging: true # 启用 public API 自动桥接(Fuse)
resources: # 自定义资源路径
- path: CustomResources
mode: copy # 'process'(默认)或 'copy'
build:
contents:
- block: 'dependencies' # Android/Gradle 依赖
contents:
- implementation("androidx.compose.material3:material3:1.2.0")

skip 块控制 Skip 处理的行为和结构,settingsbuild 块允许自定义模块的输出 settings.gradle.ktsbuild.gradle.kts 文件。这最常用于向 Gradle 项目添加 Maven 风格依赖,以便与外部依赖集成。

有关 Gradle 配置的完整参考,请参见 Gradle 项目参考

所有 Skip 项目都包含一些转译的 Skip Lite 模块。例如,SkipUI 总是被转译为 Kotlin,无论顶层应用是编译的 Skip Fuse 还是转译的 Skip Lite 应用。在 Skip Fuse 的情况下,额外的原生 SkipFuseUI 模块处理 Swift 侧到 SkipUI 模块创建的转译 Skip Lite Jetpack Compose 代码的桥接。

更一般地说,Skip Fuse 模块可以依赖 Skip Lite 模块并通过桥接代码与之交互。这就是原生编译的应用可以集成 Skip 提供的各种平台框架(如 SkipAVSkipNFCSkipBluetooth)以及第三方集成模块(如 SkipFirebaseSkipSupabaseSkipAuth0)的方式。

模块的模式由 skip.yml 决定:

skip:
mode: 'transpiled' | 'native' # 转译 Lite 或编译 Fuse 模式
bridging: true # 自动桥接所有 public API

mode 设置为 native 时,Skip 还会检查依赖树中是否存在 SkipFuseautomatic 模式(默认)在 SkipFuse 存在且模块是主应用模块时选择原生模式。

Skip Lite 模式下,转译器将 Swift 源代码转换为等效的 Kotlin 源代码。这是一个多阶段流水线,主要在 skipstone 仓库的 SkipSyntax 模块中实现。

flowchart TD
    A["1. Parse\nSwiftSyntax → Syntax Trees"] --> B["2. Decode\nSyntax Trees → Statement/Expression AST"]
    B --> C["3. Gather\nCollect types, functions, extensions\ninto CodebaseInfo"]
    C --> D["4. Prepare\nResolve types, synthesize\nconstructors, process generics"]
    D --> E["5. Translate\nSwift AST → Kotlin AST"]
    E --> F["6. Transform\nTransformer plugins applied\nin sequence"]
    F --> G["7. Output\nRender Kotlin source\nwith source mapping"]

    style A fill:#6B7280,color:#fff
    style B fill:#6B7280,color:#fff
    style C fill:#4A90D9,color:#fff
    style D fill:#4A90D9,color:#fff
    style E fill:#7B2FBE,color:#fff
    style F fill:#7B2FBE,color:#fff
    style G fill:#059669,color:#fff

Skip 使用标准 SwiftSyntax 库将 Swift 源文件解析为语法树。然后将这些语法树解码为 Skip 的内部 AST 表示——一棵由 StatementExpression 节点组成的树,捕捉代码的语义结构。

多个文件使用 Swift 的结构化并发(withThrowingTaskGroup)并行解析。

收集阶段遍历所有已解析的文件,构建 CodebaseInfo 对象——一个覆盖整个代码库的符号表,跟踪:

  • 所有类型声明(类、结构体、枚举、协议、Actor)
  • 扩展声明及其关联类型
  • 顶层函数和变量
  • 类型别名
  • 依赖模块的导出符号

每个转换器还有一个 gather() 方法,在此阶段运行以收集转换器特定的信息。

准备阶段(prepareForUse())解析类型引用、合成隐式构造函数并处理泛型。这为翻译阶段提供了代码库类型系统的完整视图。

KotlinTranslator 将每个 Swift AST 节点转换为其 Kotlin 等效形式,生成 KotlinSyntaxTree。这包括:

  • 模块名映射:Swift 模块名按 CamelCaselower.dot.separated 的约定转换为 Kotlin 包名(如 SkipFoundationskip.foundation)。可以在 skip.yml 中指定自定义映射。
  • 导入解析:Swift 导入被映射到 Kotlin 等效形式,引用 SkipLibSkipFoundationSkipUI 等框架模块。
  • 语句和表达式翻译:每个 Swift 结构被映射到其 Kotlin 对应物。

初始翻译后,约 20 个 KotlinTransformer 实现序列对 Kotlin AST 进行精化。顺序很重要——每个转换器可能依赖前面转换器的更改。这些转换器处理 Swift 和 Kotlin 语义之间的根本差异:

转换器用途
EscapeKeywords转义与 Kotlin 硬关键字冲突的标识符
OptionSet在 Kotlin 中实现 Swift 的 OptionSet 协议约定
Struct为结构体添加复制语义、变更跟踪(willmutate/didmutate)和成员初始化器
CommonProtocols移除 Kotlin 中不需要的协议一致性
CodableCodable 类型生成 encode/decode 实现
RawRepresentableRawRepresentable 类型添加工厂函数
Enum将枚举转换为 Kotlin 密封类,合成 CaseIterable
ConstructorAndSideEffectSuppression管理构造函数合成和副作用抑制
ErrorToThrowable将 Swift 的 Error 协议映射到 Kotlin 的 Throwable
Observation转换 @Observable 属性以集成 Compose 状态
IfWhenif/else 链转换为 Kotlin when 表达式
Defer实现 Swift 的 defer 语句
DisambiguateFunctions解析重载函数的歧义
TupleLabel处理元组标签语义
Concurrency转换 async/awaitTask 和结构化并发
SwiftUI将 SwiftUI 视图和修饰符转换为 Jetpack Compose
Imports解析和生成 Kotlin import 语句
UnitTest将 XCTest 断言转换为 JUnit 等效形式
Bundle处理资源包引用
FoundationBridge桥接 Foundation 框架调用
Bridge生成双向 Swift-Kotlin 互操作代码(启用桥接时)

SwiftUI 转换器将 SwiftUI 视图声明、修饰符和状态管理转换为 Jetpack Compose 等效形式。这正是 SkipUI 能够从 SwiftUI 代码渲染原生 Android UI 的原因。

Struct 转换器确保 Swift 的值类型语义(写时复制、变更跟踪)在 Kotlin 的引用类型世界中得到忠实复制。

OutputGenerator 将 Kotlin AST 渲染为源文件文本,每个 .swift 输入文件生成一个 .kt 文件。在渲染过程中,它构建一个 OutputMap,记录生成的 Kotlin 与原始 Swift 源代码之间的字节偏移映射。

这些源码映射被测试框架用于将 Kotlin 堆栈跟踪映射回 Swift 文件和行位置,使测试失败和运行时错误在 Xcode 中可以直接定位。

转译器将 Swift 类型映射到其 Kotlin/Skip 等效形式:

flowchart LR
    subgraph Swift
        S1["Int"]
        S2["String"]
        S3["Array<T>"]
        S4["Dictionary<K,V>"]
        S5["Optional<T>"]
        S6["struct"]
        S7["enum"]
    end
    subgraph Kotlin
        K1["Int / Long"]
        K2["String"]
        K3["skip.lib.Array<T>"]
        K4["skip.lib.Dictionary<K,V>"]
        K5["T?"]
        K6["class + copy semantics"]
        K7["sealed class"]
    end
    S1 --> K1
    S2 --> K2
    S3 --> K3
    S4 --> K4
    S5 --> K5
    S6 --> K6
    S7 --> K7

注意,Swift 的 Int(64 位)在 Skip Lite 中映射为 Kotlin 的 Int(32 位)。这是一个已知的静默溢出 bug 来源——详见 Swift 支持 中的数值处理详情。

ArrayDictionary 等集合类型使用 SkipLib 实现(skip.lib.Arrayskip.lib.Dictionary),而不是 Kotlin 的标准库集合,因为这些实现保留了 Swift 的值类型复制语义。

Skip Fusenative 模式下,模块的 Swift 源代码使用官方 Swift SDK for Android(Swift 6.3+)为 Android 原生编译。转译器仍然参与其中,但其角色从完整转译变为桥接生成

flowchart TD
    subgraph "Swift Side"
        A["Swift Source"] --> B["Swift SDK for Android\n(swiftc cross-compilation)"]
        B --> C["Native .so libraries"]
    end

    subgraph "Skip Processing"
        A --> D["Bridge Generator\n(skipstone)"]
        D --> E["Kotlin Bridge Wrappers\n(.kt files)"]
        D --> F["Swift Bridge Support\n(_Bridge.swift)"]
    end

    subgraph "Android Build"
        E --> G["Gradle / Kotlin Compiler"]
        C --> G
        F --> B
        G --> H["Android App\n(.apk)"]
    end

    style D fill:#7B2FBE,color:#fff
    style B fill:#4A90D9,color:#fff
    style G fill:#059669,color:#fff

在 Fuse 模式下,skip skipstone 命令将模块的 Swift 文件视为桥接文件而非转译文件。它不是逐行将 Swift 转换为 Kotlin,而是:

  1. 分析 Swift API 表面(public 类型、方法、属性)。
  2. 生成 Kotlin 桥接包装器,通过 JNI 调用原生 Swift。
  3. 生成 Swift 桥接支持文件_Bridge.swift),将 Swift 符号暴露给 JNI 层。
  4. 生成 Gradle 模块,包含 Kotlin 包装器和对原生 .so 库的引用。

桥接系统是双向的,支持 Swift 到 Kotlin 和 Kotlin 到 Swift 的调用:

flowchart LR
    subgraph "Native Swift (Android)"
        SW["Swift Code"]
        SB["_Bridge.swift\n(JNI exports)"]
    end

    subgraph "JNI Layer"
        JNI["Java Native Interface"]
    end

    subgraph "Kotlin/JVM"
        KB["Bridge Wrappers\n(generated .kt)"]
        KC["Kotlin/Java Code"]
    end

    SW <--> SB
    SB <--> JNI
    JNI <--> KB
    KB <--> KC

两个专门的访问器生成桥接代码:

  • KotlinBridgeToSwiftVisitor —— 生成 Kotlin 包装类,通过 JNI 委托方法调用到原生 Swift。
  • KotlinBridgeToKotlinVisitor —— 为需要从 Kotlin 侧访问的类型生成纯 Kotlin 代码。

桥接生成可以在不同粒度上配置——详见桥接中的逐项(@bridge)、逐类型(@bridgeMembers)和模块级(bridging: true)配置。

Skip Fuse 应用依赖一系列基础设施模块:

flowchart BT
    A["swift-jni\n(JNI C headers + Swift wrapper)"] --> B["skip-android-bridge\n(Android-specific bridging)"]
    B --> C["skip-fuse\n(OSLog, Observable,\nAnyDynamicObject)"]
    C --> D["skip-fuse-ui\n(Native Swift UI on Android)"]
    D --> E["Your Fuse App"]

    style A fill:#6B7280,color:#fff
    style B fill:#6B7280,color:#fff
    style C fill:#4A90D9,color:#fff
    style D fill:#7B2FBE,color:#fff

SkipFuse 模块提供运行时支持,包括与 Jetpack Compose 的 @Observable 状态同步。在 Fuse 模块中定义 @Observable 类型的任何 Swift 文件必须 import SkipFuse,Android UI 更新才能正常工作。

Skip 自动生成一个完整的 Gradle 项目结构,镜像你的 SwiftPM 依赖树。SkipBuild 模块中的 GradleProject 系统处理此转换。

对于每个带有 skip.yml 文件的 Swift 模块,Skip 生成一个 build.gradle.kts 文件,包括:

  • 插件:Android library 或 application 插件、Kotlin 插件、Compose 编译器插件
  • 依赖:模块间依赖(通过项目引用)和外部 Gradle 依赖(来自 skip.yml
  • 源集:指向生成的 Kotlin 源目录
  • Android 配置:最低/目标 SDK 版本、Compose 设置、ProGuard 规则

生成的 Gradle 块使用树形结构的 GradleBlock 表示,支持合并——来自 skip.yml 配置的同名块与生成的默认值合并,允许细粒度自定义。

除了每个模块的 build.gradle.kts 文件,Skip 还生成:

文件用途
settings.gradle.kts模块包含、插件管理、桥接模块列表
gradle.propertiesJVM 参数、AndroidX 标志、来自 skip.yml 的自定义属性
gradle/wrapper/gradle-wrapper.propertiesGradle 版本锁定

对于应用项目,顶层 Android/ 目录包含可手动编辑的 Gradle 配置,其中包含生成的模块。完整结构请参见 Gradle 项目参考

Skip 创建一个镜像 SwiftPM 依赖树的 Gradle 依赖树:

flowchart TD
    subgraph "Gradle Modules"
        GA["your.app"] --> GM["your.model"]
        GA --> GUI["skip.ui"]
        GM --> GF["skip.foundation"]
        GUI --> GF
        GF --> GL["skip.lib"]
    end
    subgraph "SwiftPM Modules"
        YA["YourApp"] --> YM["YourModel"]
        YA --> SUI["SkipUI"]
        YM --> SF["SkipFoundation"]
        SUI --> SF
        SF --> SL["SkipLib"]
    end
    

    YA -.->|"generates"| GA
    YM -.->|"generates"| GM

    style YA fill:#4A90D9,color:#fff
    style GA fill:#059669,color:#fff
    style YM fill:#4A90D9,color:#fff
    style GM fill:#059669,color:#fff

每个框架模块——SkipLibSkipFoundationSkipModelSkipUISkipUnit——由插件处理并通过符号链接链接到 Gradle 树中。

Skip 处理来自 Swift 模块的资源,并通过 Gradle 的 asset 和 resource 系统使其可用于 Android 构建。

资源通过以下方式定位:

  1. 默认:模块源文件夹中的 Resources/ 目录。
  2. 显式配置:在 skip.ymlskip.resources 下指定的路径。
模式行为用例
Process(默认)扁平化目录层次结构;将 .xcstrings 转换为 Android strings.xml标准应用资源、可本地化字符串
Copy保持目录层次结构不变预结构化资源、自定义文件布局

处理后的资源输出到 src/main/assets/<package>/<name>/,通过 Android 的 AssetManager 访问,或输出到 src/main/res/ 用于本地化字符串等 Android 资源类型。

Xcode 的 .xcstrings 文件(字符串目录)在处理阶段自动转换为 Android 的 values/strings.xml 格式。这允许一套本地化文件同时服务两个平台。

Skip 不是复制资源文件,而是从 Gradle 输出目录创建符号链接回到原始源文件。这意味着对资源的编辑会立即反映在下一次 Android 构建中,无需重新转译。只读资源(来自依赖)则被复制。

Skip 与 Xcode 的测试运行器集成,在 JVM 或 Android 上执行转译或编译的测试。测试基础设施由 skip 仓库中的 SkipTestSkipDrive 模块提供。

sequenceDiagram
    participant Xcode
    participant XCTest as XCSkipTests
    participant Gradle
    participant JVM as JVM / Robolectric

    Xcode->>XCTest: Run test target
    XCTest->>Gradle: Execute testDebug / connectedAndroidTest
    Gradle->>JVM: Run transpiled JUnit tests
    JVM-->>Gradle: JUnit XML results
    Gradle-->>XCTest: Parse test results
    XCTest-->>Xcode: Report as XCTest failures

Lite 测试目标中的每个 XCTestCase 子类都会由 SkipUnit 转换器自动转译为 Kotlin/JUnit 测试类。XCSkipTests 框架(如果不存在则自动生成)协调执行:

  1. Robolectric(默认,无需设备):使用 Robolectric 在本地 JVM 上运行转译的测试,模拟 Android API。通过 Gradle 的 testDebug 动作调用。
  2. Instrumented(设置 ANDROID_SERIAL):通过 connectedDebugAndroidTest 在真实 Android 设备或模拟器上部署和运行测试。

Fuse 测试被交叉编译为 Android 原生 Swift 并通过 adb 执行:

  • CLI 模式skip android test):将裸可执行文件推送到设备。支持资源包但不支持 Android 框架 API。
  • APK 模式skip android test --apk):将测试打包在 APK 中,具有完整的 JNI 和 Android 框架访问权限。

完整测试指南请参见测试

当 Kotlin 测试失败或发生运行时错误时,SkipDrive 中的 GradleDriver 逐行解析 Gradle 输出,提取 Kotlin 文件路径和行号。然后使用输出阶段生成的源码映射将这些翻译回原始 Swift 源位置,使失败在 Xcode 中显示在正确的 Swift 文件和行处。

当你在 Xcode 中为 Skip 项目按下运行时,会执行以下序列:

sequenceDiagram
    participant Dev as Developer
    participant Xcode
    participant Plugin as skipstone Plugin
    participant CLI as skip CLI
    participant Gradle
    participant Emulator as Android Emulator

    Dev->>Xcode: Press Run (⌘R)
    Xcode->>Xcode: Build iOS target normally

    par iOS Build
        Xcode->>Xcode: Compile Swift for iOS
        Xcode->>Xcode: Link and sign iOS app
        Xcode->>Xcode: Launch on iOS Simulator
    and Android Build
        Xcode->>Plugin: Build plugin targets
        Plugin->>CLI: skip skipstone (per module)
        CLI->>CLI: Transpile/bridge Swift → Kotlin
        CLI->>CLI: Generate Gradle files
        CLI-->>Plugin: .sourcehash markers
        Plugin-->>Xcode: Build commands complete
        Xcode->>Gradle: Build Android project
        Gradle->>Gradle: Compile Kotlin
        Gradle->>Gradle: Package APK
        Gradle->>Emulator: Install and launch APK
    end

iOS 应用的 .xcconfig 文件通过 SKIP_ACTION 设置控制 Android 构建行为:

行为
launch(默认)构建并在 Android 模拟器/设备上启动
build构建 APK 但不启动
none完全跳过 Android 构建(仅 iOS 迭代)

有关特定主题的更多详情: