【Compose Multiplatform跨端声明式UI实用技术】第19课:多平台部署与发布

第18课完成了测试策略的学习。本课进入部署与发布模块:四端发布全景与约束差异、Android的签名配置与Google Play发布、iOS的Xcode归档与TestFlight/App Store发布、Desktop的jpackage原生安装包打包、Web(Wasm)的静态托管部署,以及CI/CD流水线设计与版本管理策略。本课内容基于CMP 1.12.1、Kotlin 2.3.20与AGP 9.0。

目录

  • 一、发布全景:四端分发的差异与约束
  • [二、Android:签名配置与Google Play发布](#二、Android:签名配置与Google Play发布 "#%E4%BA%8Candroid%E7%AD%BE%E5%90%8D%E9%85%8D%E7%BD%AE%E4%B8%8Egoogle-play%E5%8F%91%E5%B8%83")
    • [2.1 生成签名密钥](#2.1 生成签名密钥 "#21-%E7%94%9F%E6%88%90%E7%AD%BE%E5%90%8D%E5%AF%86%E9%92%A5")
    • [2.2 配置签名](#2.2 配置签名 "#22-%E9%85%8D%E7%BD%AE%E7%AD%BE%E5%90%8D")
    • [2.3 构建AAB](#2.3 构建AAB "#23-%E6%9E%84%E5%BB%BAaab")
    • [2.4 本地验证AAB](#2.4 本地验证AAB "#24-%E6%9C%AC%E5%9C%B0%E9%AA%8C%E8%AF%81aab")
    • [2.5 使用Fastlane自动化Google Play发布](#2.5 使用Fastlane自动化Google Play发布 "#25-%E4%BD%BF%E7%94%A8fastlane%E8%87%AA%E5%8A%A8%E5%8C%96google-play%E5%8F%91%E5%B8%83")
  • 三、iOS:Xcode归档与TestFlight发布
    • [3.1 构建前置条件](#3.1 构建前置条件 "#31-%E6%9E%84%E5%BB%BA%E5%89%8D%E7%BD%AE%E6%9D%A1%E4%BB%B6")
    • [3.2 在Xcode中归档](#3.2 在Xcode中归档 "#32-%E5%9C%A8xcode%E4%B8%AD%E5%BD%92%E6%A1%A3")
    • [3.3 使用Fastlane自动化iOS发布](#3.3 使用Fastlane自动化iOS发布 "#33-%E4%BD%BF%E7%94%A8fastlane%E8%87%AA%E5%8A%A8%E5%8C%96ios%E5%8F%91%E5%B8%83")
    • [3.4 版本号管理](#3.4 版本号管理 "#34-%E7%89%88%E6%9C%AC%E5%8F%B7%E7%AE%A1%E7%90%86")
  • 四、Desktop:jpackage原生安装包
    • [4.1 Compose Multiplatform Gradle插件的nativeDistributions配置](#4.1 Compose Multiplatform Gradle插件的nativeDistributions配置 "#41-compose-multiplatform-gradle%E6%8F%92%E4%BB%B6%E7%9A%84nativedistributions%E9%85%8D%E7%BD%AE")
    • [4.2 构建命令](#4.2 构建命令 "#42-%E6%9E%84%E5%BB%BA%E5%91%BD%E4%BB%A4")
    • [4.3 jDeploy:免jpackage的替代方案](#4.3 jDeploy:免jpackage的替代方案 "#43-jdeploy%E5%85%8Djpackage%E7%9A%84%E6%9B%BF%E4%BB%A3%E6%96%B9%E6%A1%88")
    • [4.4 macOS签名与公证](#4.4 macOS签名与公证 "#44-macos%E7%AD%BE%E5%90%8D%E4%B8%8E%E5%85%AC%E8%AF%81")
  • 五、Web(Wasm):静态托管部署
    • [5.1 构建Wasm产物](#5.1 构建Wasm产物 "#51-%E6%9E%84%E5%BB%BAwasm%E4%BA%A7%E7%89%A9")
    • [5.2 部署到静态托管](#5.2 部署到静态托管 "#52-%E9%83%A8%E7%BD%B2%E5%88%B0%E9%9D%99%E6%80%81%E6%89%98%E7%AE%A1")
    • [5.3 部署到GitHub Pages](#5.3 部署到GitHub Pages "#53-%E9%83%A8%E7%BD%B2%E5%88%B0github-pages")
    • [5.4 部署到Cloudflare Pages](#5.4 部署到Cloudflare Pages "#54-%E9%83%A8%E7%BD%B2%E5%88%B0cloudflare-pages")
  • 六、CI/CD流水线设计
    • [6.1 三种主流方案对比](#6.1 三种主流方案对比 "#61-%E4%B8%89%E7%A7%8D%E4%B8%BB%E6%B5%81%E6%96%B9%E6%A1%88%E5%AF%B9%E6%AF%94")
    • [6.2 三阶段发布流程](#6.2 三阶段发布流程 "#62-%E4%B8%89%E9%98%B6%E6%AE%B5%E5%8F%91%E5%B8%83%E6%B5%81%E7%A8%8B")
    • [6.3 完整的GitHub Actions工作流](#6.3 完整的GitHub Actions工作流 "#63-%E5%AE%8C%E6%95%B4%E7%9A%84github-actions%E5%B7%A5%E4%BD%9C%E6%B5%81")
    • [6.4 多平台CI矩阵](#6.4 多平台CI矩阵 "#64-%E5%A4%9A%E5%B9%B3%E5%8F%B0ci%E7%9F%A9%E9%98%B5")
    • [6.5 版本管理策略](#6.5 版本管理策略 "#65-%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E7%AD%96%E7%95%A5")
    • [6.6 签名与密钥管理](#6.6 签名与密钥管理 "#66-%E7%AD%BE%E5%90%8D%E4%B8%8E%E5%AF%86%E9%92%A5%E7%AE%A1%E7%90%86")
  • 配套习题(15题)
  • 本课知识点总结

文末附有15道配套习题与本课知识点总结。

一、发布全景:四端分发的差异与约束

CMP应用发布到四个平台,每个平台的分发机制、约束条件和产物格式都不同。理解这些差异,是规划发布流程的前提。

平台 分发渠道 产物格式 核心约束 成熟度
Android Google Play / 国内应用商店 APK / AAB keystore签名,versionCode递增 Stable
iOS App Store / TestFlight IPA 必须在macOS上构建,需要Apple Developer账号、Xcode签名和公证 Stable(CMP 1.8.0起)
Desktop 官网 / GitHub Releases .msi/.exe(Windows)、.dmg/.pkg(macOS)、.deb/.rpm(Linux) 不支持交叉编译,各平台需在对应操作系统上构建,打包需JDK 17+ Stable
Web 静态托管(GitHub Pages、Cloudflare Pages、Vercel、Netlify) HTML + JS + Wasm 需配置SPA重写规则和.wasm MIME类型,Web目标为Beta Beta

三个关键跨平台约束:

桌面端不支持交叉编译 。jpackage工具只能为当前操作系统生成安装包------只能在macOS上构建.dmg,在Windows上构建.msi,在Linux上构建.deb。发布全平台桌面应用需要三种操作系统(或CI环境)。官方打包工具jpackage要求JDK 17或更高版本。

iOS构建必须在macOS上进行。这是苹果生态的硬性限制,CMP代码需要编译为Native二进制并通过Xcode签名和归档。

Web目标为Beta状态。CMP的Web目标(Wasm)在CMP 1.9.0中进入Beta,建议先发布Android/iOS/Desktop,Web作为试点。

下面是四端发布全景图:

flowchart TD A["CMP 应用源码"] --> B{"选择发布平台"} B -->|"Android"| C["Google Play / 国内商店"] B -->|"iOS"| D["App Store / TestFlight"] B -->|"Desktop"| E["官网 / GitHub Releases"] B -->|"Web"| F["静态托管"] C --> C1["产物: APK / AAB"] C1 --> C2["约束: keystore签名, versionCode递增"] D --> D1["产物: IPA"] D1 --> D2["约束: 必须在macOS上构建"] E --> E1["产物: .dmg / .msi / .deb"] E1 --> E2["约束: 不支持交叉编译, JDK 17+"] F --> F1["产物: HTML + JS + Wasm"] F1 --> F2["约束: SPA重写规则, .wasm MIME类型"] C2 & D2 & E2 & F2 --> G["CI/CD 流水线统一管理"]

二、Android:签名配置与Google Play发布

2.1 生成签名密钥

Android应用必须用密钥签名才能安装到设备或上传到商店。使用keytool生成:

bash 复制代码
keytool -genkey -v -keystore my-release-key.keystore \
  -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

2.2 配置签名

在androidApp/build.gradle.kts中配置签名,通过环境变量读取敏感信息:

kotlin 复制代码
android {
    signingConfigs {
        create("release") {
            storeFile = file(System.getenv("KEYSTORE_PATH") ?: "my-release-key.keystore")
            storePassword = System.getenv("KEYSTORE_PASSWORD")
            keyAlias = System.getenv("KEYSTORE_ALIAS")
            keyPassword = System.getenv("KEY_PASSWORD")
        }
    }
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

关键实践 :签名密钥文件(.keystore/.jks)绝不提交到版本控制 ,应加入.gitignore。密码通过环境变量注入,在CI中通过GitHub Secrets管理。

2.3 构建AAB

Google Play要求上传AAB(Android App Bundle),而非APK。AAB包含所有编译代码和资源,Play Store会根据设备配置生成优化的APK,减小下载体积。

bash 复制代码
# 构建Release AAB
./gradlew :androidApp:bundleRelease

# 输出位置:androidApp/build/outputs/bundle/release/androidApp-release.aab

Google Play支持最大4GB的累计下载大小限制。如果应用启用了代码混淆,最佳实践是在发布时上传解混淆文件(mapping文件)。

2.4 本地验证AAB

上传前可使用bundletool在本地验证AAB的正确性:

bash 复制代码
# 从AAB生成APK集合
java -jar bundletool.jar build-apks \
  --bundle=androidApp-release.aab \
  --output=my_app.apks \
  --ks=my-release-key.keystore \
  --ks-pass=pass:KEYSTORE_PASSWORD \
  --ks-key-alias=my-key-alias \
  --key-pass=pass:KEY_PASSWORD

# 安装到连接的设备
java -jar bundletool.jar install-apks --apks=my_app.apks

2.5 使用Fastlane自动化Google Play发布

Fastlane的supply lane可以自动化上传AAB到Google Play。配置需要Google Play Console的服务账号JSON密钥:

ruby 复制代码
# fastlane/Fastfile
platform :android do
  desc "Deploy to Internal Testing Track"
  lane :deployInternal do
    upload_to_play_store(
      track: 'internal',
      aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
      json_key: 'secrets/playStorePublishServiceCredentialsFile.json',
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

下面是 Android 发布流程:

flowchart LR A["keytool 生成签名密钥"] --> B["build.gradle.kts 配置签名"] B --> C["构建 AAB 产物"] C --> D["bundletool 本地验证"] D --> E{"验证通过?"} E -->|"否"| C E -->|"是"| F["Fastlane supply 上传"] F --> G["Google Play Internal Track"]

三、iOS:Xcode归档与TestFlight发布

3.1 构建前置条件

iOS发布需要以下准备:

  • Apple Developer账号(个人或组织)
  • macOS主机------iOS构建必须在macOS上进行
  • Xcode------用于归档、签名和上传
  • App ID与Provisioning Profile------在Apple Developer Portal中配置

3.2 在Xcode中归档

iOS的发布流程通过Xcode的Organizer完成:

  1. 在Xcode中打开iosApp/iosApp.xcodeproj
  2. 选择 Product → Archive,构建Release版本并生成归档
  3. 在Organizer中,点击 Validate App 验证归档
  4. 验证通过后,点击 Distribute App 上传到App Store Connect

关键配置:在Xcode的Signing & Capabilities中,确保选择正确的Team和Provisioning Profile。对于Compose Multiplatform iOS应用,需要确保Kotlin/Native生成的framework被正确嵌入并签名(Embed & Sign)。

macOS App Store的额外约束:Apple要求每个二进制文件都被签名------包括应用主体、JVM运行时、Kotlin原生库、每个.dylib。每个都需要Provisioning Profile,一个未签名的文件就会导致拒绝。推荐的做法是跳过Mac App Store,通过签名并公证的DMG在GitHub Releases分发。

3.3 使用Fastlane自动化iOS发布

Fastlane的pilot lane可以自动化TestFlight上传,使用App Store Connect API Key进行认证:

ruby 复制代码
# fastlane/Fastfile
platform :ios do
  desc "Build and upload to TestFlight"
  lane :upload_testflight do
    build_app(
      scheme: "iosApp",
      export_method: "app-store",
      output_directory: "build/ios"
    )
    upload_to_testflight(
      api_key_path: "secrets/asc_api_key.json",
      skip_waiting_for_build_processing: true
    )
  end
end

已知陷阱:版本注入静默失败 。当KMP项目中GENERATE_INFOPLIST_FILE = true时,Xcode会动态生成Info.plist,导致Fastlane的increment_version_number等操作无效。解决方案有两种:将GENERATE_INFOPLIST_FILE设为false,或直接在Fastfile中通过xcargs注入版本属性:

ruby 复制代码
build_app(
  xcargs: [
    "MARKETING_VERSION=#{sanitized_version}",
    "CURRENT_PROJECT_VERSION=#{build_number}"
  ].join(" ")
)

3.4 版本号管理

iOS的版本号由MARKETING_VERSION(市场版本号,如1.0.0)和CURRENT_PROJECT_VERSION(构建号,如42)组成。构建号必须在每次上传时递增。在CI中,可以通过脚本自动计算下一个构建号------取App Store Connect最新构建号和GitHub Release构建号的最大值加1。

下面是 iOS 发布流程:

flowchart LR A["构建前置条件"] --> B["Xcode Product → Archive"] B --> C["Organizer Validate App"] C --> D{"验证通过?"} D -->|"否"| B D -->|"是"| E["Distribute App"] E --> F["Fastlane pilot 上传"] F --> G["TestFlight / App Store"] H["版本号管理"] --> I["MARKETING_VERSION"] H --> J["CURRENT_PROJECT_VERSION 递增"]

四、Desktop:jpackage原生安装包

4.1 Compose Multiplatform Gradle插件的nativeDistributions配置

Compose Multiplatform Gradle插件基于jpackage和jlink生成自包含的原生安装包,无需目标系统安装JDK。打包需要JDK 17或更高版本。

kotlin 复制代码
compose.desktop {
    application {
        mainClass = "com.example.app.MainKt"
        nativeDistributions {
            targetFormats(
                TargetFormat.Dmg,    // macOS
                TargetFormat.Msi,    // Windows
                TargetFormat.Deb     // Linux
            )
            packageName = "com.example.app"
            packageVersion = "1.0.0"
        }
    }
}

支持的安装包格式:

平台 格式 TargetFormat
macOS .dmg / .pkg TargetFormat.Dmg / TargetFormat.Pkg
Windows .msi / .exe TargetFormat.Msi / TargetFormat.Exe
Linux .deb / .rpm TargetFormat.Deb / TargetFormat.Rpm

4.2 构建命令

bash 复制代码
# 构建当前操作系统的原生安装包
./gradlew :desktopApp:packageDistributionForCurrentOS

# 构建指定格式
./gradlew :desktopApp:packageDmg
./gradlew :desktopApp:packageMsi
./gradlew :desktopApp:packageDeb

关键约束 :jpackage不支持交叉编译。必须在macOS上构建.dmg,在Windows上构建.msi,在Linux上构建.deb。在CI中,需要为每个平台配置对应的runner。

JDK版本一致性:在Mac和Windows上必须使用相同的JDK版本(推荐JDK 17)。如果开发阶段Mac使用JDK 17而Windows使用不同版本,可能在release阶段出现编译错误。

4.3 jDeploy:免jpackage的替代方案

jDeploy是一个第三方工具,可以将Kotlin Compose Multiplatform桌面应用打包为原生安装包,并自动发布到GitHub Releases。jDeploy的优势在于简化了配置流程和自动更新支持。使用jDeploy Desktop App或CLI,可以自动完成Shadow插件配置、跨平台依赖设置和GitHub Actions工作流生成。

4.4 macOS签名与公证

macOS的.dmg分发需要进行代码签名和公证(Notarization),否则用户打开时会看到安全警告:

kotlin 复制代码
macOS {
    signing {
        sign.set(true)
        identity.set("Developer ID Application: Your Name (TEAM_ID)")
    }
    notarization {
        appleID.set(System.getenv("APPLE_ID"))
        password.set(System.getenv("APPLE_APP_PASSWORD"))
        teamID.set(System.getenv("APPLE_TEAM_ID"))
    }
}

下面是 Desktop 打包流程:

flowchart TD A["Compose Desktop 配置"] --> B["nativeDistributions"] B --> C{"目标平台"} C -->|"macOS"| D["构建 .dmg / .pkg"] C -->|"Windows"| E["构建 .msi / .exe"] C -->|"Linux"| F["构建 .deb / .rpm"] D --> G["macOS 签名与公证"] G --> H["GitHub Releases 分发"] E --> H F --> H I["jDeploy 替代方案"] --> J["自动生成 GitHub Actions 工作流"] J --> H

五、Web(Wasm):静态托管部署

5.1 构建Wasm产物

Web端CMP应用通过wasmJsBrowserDistribution任务构建:

bash 复制代码
./gradlew :webApp:wasmJsBrowserDistribution

构建产物位于webApp/build/dist/wasmJs/productionExecutable/,包含index.html、.js和.wasm文件。将该目录的内容复制到任意静态托管服务即可。

5.2 部署到静态托管

SPA重写规则 是必需的配置。CMP的Web应用使用前端路由,服务器需要将所有路径重写到index.html。官方文档明确要求"服务器必须为任何不匹配文件的路径提供index.html"。

在Vercel中,通过vercel.json配置:

json 复制代码
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ],
  "headers": [
    {
      "source": "/**.wasm",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    }
  ]
}

MIME类型配置 :确保服务器正确识别.wasm文件的MIME类型为application/wasm。Apache服务器需要在.htaccess中添加AddType application/wasm .wasm。如果服务器未正确配置,页面会白屏。

缓存策略 :.wasm文件的文件名包含内容哈希,可以长期缓存(immutable)。webApp.js文件名固定,需要每次检查更新。

5.3 部署到GitHub Pages

yaml 复制代码
# .github/workflows/deploy-web.yml
- name: Build Wasm
  run: ./gradlew :webApp:wasmJsBrowserDistribution

- name: Deploy to GitHub Pages
  uses: peaceiris/actions-gh-pages@v4
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: webApp/build/dist/wasmJs/productionExecutable

5.4 部署到Cloudflare Pages

已知陷阱:tag部署需要指定branch 。从git tag部署时,Cloudflare不知道对应的分支,部署会到preview URL而非production。必须在Wrangler Action中指定branch: main:

yaml 复制代码
- name: Deploy to Cloudflare
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    command: pages deploy ./webApp/build/dist/wasmJs/productionExecutable --project-name=${{ github.event.repository.name }} --branch=main

下面是 Web 部署流程:

flowchart LR A["wasmJsBrowserDistribution 构建"] --> B["产物目录"] B --> C{"选择托管平台"} C -->|"Vercel"| D["vercel.json 配置"] C -->|"GitHub Pages"| E["peaceiris/actions-gh-pages"] C -->|"Cloudflare Pages"| F["wrangler-action"] D --> G["SPA 重写规则"] E --> G F --> G G --> H["MIME 类型配置"] H --> I["缓存策略"] I --> J["上线"]

六、CI/CD流水线设计

6.1 三种主流方案对比

方案 优势 适用场景
GitHub Actions 灵活、贴近仓库、成本效益高 代码托管在GitHub的团队
Bitrise 移动发布管理成熟、签名和商店交付开箱即用 需要简化移动发布流程的团队
Fastlane + 自建Runner 完全控制、可定制 需要深度定制发布流程的团队

对于大多数CMP项目,GitHub Actions是首选------它灵活、贴近仓库,且对许多团队而言成本效益更高。

6.2 三阶段发布流程

生产级CMP项目的CI/CD采用三阶段发布流程:

阶段 触发方式 执行内容
Cut(切分支) 手动触发 从main创建prod/{version}分支,main的版本号递增
Deploy RC 推送到prod/*自动触发 打RC标签,上传AAB到Play Internal Track,上传IPA到TestFlight
Tag Final QA通过后手动触发 在prod分支打最终v{version}标签,创建GitHub Release

6.3 完整的GitHub Actions工作流

JetBrains官方推荐的KMP CI配置建议创建复合Action 来统一Java和Gradle设置,主工作流在每次push或PR到main分支时运行共享测试(jvmTest)并触发各平台构建。

yaml 复制代码
# .github/actions/gradle-setup/action.yml
name: gradle-setup
description: Setup Java and Gradle
runs:
  using: "composite"
  steps:
    - name: Setup Java
      uses: actions/setup-java@v4
      with:
        java-version: "17"
        distribution: "temurin"
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@v5
yaml 复制代码
# .github/workflows/build.yml
name: Build & Release
on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

env:
  GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4096M -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true"

jobs:
  # Job 1: 共享测试
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Gradle setup
        uses: ./.github/actions/gradle-setup
      - name: Run unit tests
        run: ./gradlew jvmTest
      - name: Upload test reports
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: "**/build/reports/tests/"

  # Job 2: 代码质量
  code-quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Detekt
        run: ./gradlew detekt

  # Job 3: Android构建与签名
  build-android:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Decode Keystore
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
      - name: Build Release AAB
        env:
          KEYSTORE_PATH: keystore.jks
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: ./gradlew :androidApp:bundleRelease
      - name: Upload AAB
        uses: actions/upload-artifact@v4
        with:
          name: android-aab
          path: androidApp/build/outputs/bundle/release/*.aab

  # Job 4: iOS构建
  build-ios:
    needs: test
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - name: Gradle setup
        uses: ./.github/actions/gradle-setup
      - name: Build iOS Framework
        run: ./gradlew :shared:linkReleaseFrameworkIosArm64
      - name: Archive iOS App
        run: |
          xcodebuild archive \
            -project iosApp/iosApp.xcodeproj \
            -scheme iosApp \
            -archivePath build/iosApp.xcarchive \
            -destination "generic/platform=iOS"
      - name: Export IPA
        run: |
          xcodebuild -exportArchive \
            -archivePath build/iosApp.xcarchive \
            -exportOptionsPlist iosApp/ExportOptions.plist \
            -exportPath build/ios
      - name: Upload to TestFlight
        run: fastlane ios upload_testflight

  # Job 5: Web构建与部署
  build-web:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Wasm
        run: ./gradlew :webApp:wasmJsBrowserDistribution
      - name: Deploy to Static Host
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: webApp/build/dist/wasmJs/productionExecutable

6.4 多平台CI矩阵

Job Runner 执行内容
test-shared ubuntu-latest commonTest + jvmTest(单元测试 + Compose UI测试)
test-android ubuntu-latest androidHostTest(Roborazzi截图测试)
test-ios macos-latest iosSimulatorArm64Test + Xcode UI Tests
build-android ubuntu-latest androidApp:bundleRelease
build-ios macos-latest Xcode archive + TestFlight
build-web ubuntu-latest wasmJsBrowserDistribution + 静态托管部署

6.5 版本管理策略

CMP项目涉及多个平台的版本号,需要统一管理:

平台 版本号位置 说明
Android build.gradle.kts的versionName / versionCode versionCode必须递增
iOS Config.xcconfig的MARKETING_VERSION / CURRENT_PROJECT_VERSION 构建号必须递增
Desktop nativeDistributions.packageVersion 语义化版本号
Web 无强制版本要求 可通过构建号标记

推荐通过scripts/ios_next_build_number.py等脚本统一计算和同步各平台的版本号。

6.6 签名与密钥管理

CI/CD中的签名密钥通过GitHub Secrets管理。Android签名需要以下Secrets:

Secret名称 说明
KEYSTORE_BASE64 Base64编码的keystore文件
KEYSTORE_PASSWORD keystore密码
KEYSTORE_ALIAS keystore别名
KEY_PASSWORD 密钥密码
PLAYSTORE_CREDS Google Play服务账号JSON

iOS签名需要:

Secret名称 说明
ASC_API_KEY App Store Connect API Key(JSON)
APPLE_ID Apple ID(用于公证)
APPLE_APP_PASSWORD App专用密码

安全实践:keystore文件在CI中通过Base64解码临时生成,构建完成后删除;所有密码通过环境变量注入,绝不硬编码在构建脚本中。

Detekt的KMP配置陷阱 :./gradlew detekt可能静默跳过KMP代码,返回成功但不检查任何内容。必须配置源集特定的任务:detektMetadataCommonMain、detektAndroidMain、detektIosArm64Main、detektDesktopMain、detektWasmJsMain。其中detektMetadataCommonMain检查的是共享代码,缺少它会导致CI绿灯但实际检查了零行代码。

配套习题(15题)

第一组:发布全景与Android部署(4题)

第1题 桌面端CMP应用发布的核心约束是( )

A. 只能在Linux上构建所有平台安装包 B. 不支持交叉编译,各平台需在对应操作系统上构建 C. 只能发布到GitHub Releases D. 需要目标系统安装JDK

答案:B

解析:桌面端不支持交叉编译------只能在macOS上构建.dmg,在Windows上构建.msi,在Linux上构建.deb。发布全平台桌面应用需要三种操作系统(或CI环境)。

第2题 Google Play要求上传的Android产物格式是( )

A. APK B. AAB C. IPA D. JAR

答案:B

解析:Google Play要求上传AAB(Android App Bundle),而非APK。AAB包含所有编译代码和资源,Play Store会根据设备配置生成优化的APK,减小下载体积。

第3题 Android签名密钥文件的正确管理方式是( )

A. 提交到版本控制,方便团队共享 B. 加入.gitignore,密码通过环境变量注入 C. 放在项目根目录并硬编码密码 D. 通过邮件发送给团队成员

答案:B

解析:签名密钥文件(.keystore/.jks)绝不提交到版本控制,应加入.gitignore。密码通过环境变量注入,在CI中通过GitHub Secrets管理。

第4题 用于本地验证AAB正确性的工具是( )

A. jpackage B. bundletool C. keytool D. fastlane

答案:B

解析:使用bundletool可以从AAB生成APK集合,并安装到连接的设备进行本地验证。keytool用于生成签名密钥,jpackage用于桌面端打包,fastlane用于自动化发布。

第二组:iOS部署(4题)

第5题 iOS应用构建的硬性限制是( )

A. 只能在Linux上构建 B. 必须在macOS上进行构建 C. 只能在Windows上构建 D. 不需要特定操作系统

答案:B

解析:iOS构建必须在macOS上进行,这是苹果生态的硬性限制。CMP代码被编译为Native二进制,需要Xcode进行签名和归档。

第6题 iOS发布流程中,归档后验证应用的正确步骤是( )

A. 直接上传到App Store B. 在Organizer中点击Validate App验证 C. 在模拟器中运行 D. 使用bundletool验证

答案:B

解析:在Xcode的Organizer中,归档后需要先点击Validate App验证归档,验证通过后再点击Distribute App上传到App Store Connect。

第7题 macOS App Store对KMP应用的特殊约束是( )

A. 无需任何额外处理 B. 每个二进制文件(包括JVM运行时和.dylib)都需要签名和Provisioning Profile C. 只需签名应用主体 D. 可以使用未签名的二进制文件

答案:B

解析:Apple要求每个二进制文件都被签名------包括应用主体、JVM运行时、Kotlin原生库、每个.dylib。每个都需要Provisioning Profile,一个未签名的文件就会导致拒绝。推荐跳过Mac App Store,通过签名并公证的DMG在GitHub Releases分发。

第8题 当KMP项目中GENERATE_INFOPLIST_FILE = true时,Fastlane版本注入失败的解决方案是( )

A. 手动修改Info.plist B. 设置GENERATE_INFOPLIST_FILE = false或通过xcargs注入版本属性 C. 使用bundletool注入 D. 不需要处理

答案:B

解析 :当GENERATE_INFOPLIST_FILE = true时,Xcode会动态生成Info.plist,导致Fastlane的版本注入操作无效。解决方案是将GENERATE_INFOPLIST_FILE设为false,或直接在Fastfile中通过xcargs注入MARKETING_VERSION和CURRENT_PROJECT_VERSION。

第三组:Desktop与Web部署(4题)

第9题 Compose Multiplatform Gradle插件生成桌面原生安装包基于什么工具?( )

A. Electron B. jpackage和jlink C. Flutter D. Docker

答案:B

解析:Compose Multiplatform Gradle插件基于jpackage和jlink生成自包含的原生安装包,无需目标系统安装JDK。打包需要JDK 17或更高版本。

第10题 macOS的.dmg分发需要进行的额外处理是( )

A. 无需额外处理 B. 代码签名和公证(Notarization) C. 转换为APK D. 上传到Google Play

答案:B

解析:macOS的.dmg分发需要进行代码签名和公证(Notarization),否则用户打开时会看到安全警告。

第11题 Web端CMP应用的构建任务是( )

A. jsBrowserDistribution B. wasmJsBrowserDistribution C. browserDistribute D. webBuild

答案:B

解析 :Web端CMP应用通过wasmJsBrowserDistribution任务构建,产物位于build/dist/wasmJs/productionExecutable/。

第12题 从git tag部署到Cloudflare Pages时的已知陷阱是( )

A. 不支持tag部署 B. 需要指定branch: main,否则部署会到preview URL而非production C. 需要额外安装插件 D. 只支持APK格式

答案:B

解析 :从git tag部署时,Cloudflare不知道对应的分支,部署会到preview URL而非production。必须在Wrangler Action中指定branch: main。

第四组:CI/CD与版本管理(3题)

第13题 大多数CMP项目首选的CI/CD方案是( )

A. Bitrise B. GitHub Actions C. Jenkins D. Travis CI

答案:B

解析:对于大多数CMP项目,GitHub Actions是首选------它灵活、贴近仓库,且对许多团队而言成本效益更高。Bitrise适用于需要简化移动发布流程的团队。

第14题 关于Detekt在KMP项目中的行为,以下说法正确的是( )

A. ./gradlew detekt会自动检查所有KMP源集 B. ./gradlew detekt可能静默跳过KMP代码,需要配置源集特定任务 C. Detekt不支持KMP项目 D. 只需运行detektMetadataCommonMain即可

答案:B

解析 :./gradlew detekt可能静默跳过KMP代码,返回成功但不检查任何内容。必须配置源集特定的任务:detektMetadataCommonMain、detektAndroidMain、detektIosArm64Main、detektDesktopMain、detektWasmJsMain。其中detektMetadataCommonMain检查共享代码。

第15题 CI环境中Gradle构建优化的关键配置不包括( )

A. 禁用Gradle daemon B. 启用并行执行 C. 启用构建缓存 D. 启用Gradle daemon以提高性能

答案:D

解析 :在CI环境中,应禁用Gradle daemon(-Dorg.gradle.daemon=false),因为每个构建在独立的runner上执行,daemon无法复用。同时启用并行执行和构建缓存以提高效率。JetBrains官方推荐的配置为:GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4096M -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true"。

本课知识点总结

1. 四端分发的差异与约束

CMP应用发布到四个平台,各平台分发机制不同。Android通过APK/AAB分发,使用keystore签名。iOS通过IPA分发到App Store,必须在macOS上构建 ,需要Apple Developer账号、Xcode签名和公证。Desktop通过原生安装包分发,不支持交叉编译 ,各平台需在对应操作系统上构建。Web通过静态文件托管分发,需配置SPA重写规则和.wasm MIME类型。桌面端打包需要JDK 17+,iOS已在CMP 1.8.0达到Stable,Web目标为Beta。

2. Android签名与发布

使用keytool生成签名密钥,在build.gradle.kts中通过环境变量配置签名信息。签名密钥文件绝不提交版本控制 。构建AAB使用./gradlew :androidApp:bundleRelease,使用bundletool本地验证。上传到Google Play通过Play Console或Fastlane的supply lane自动化。AAB是Google Play要求的唯一格式,Play Store根据设备生成优化APK。

3. iOS归档与TestFlight发布

iOS发布通过Xcode的Product → Archive → Validate App → Distribute App完成。Fastlane的pilot lane使用App Store Connect API Key自动化TestFlight上传。已知陷阱 :GENERATE_INFOPLIST_FILE = true时Fastlane版本注入静默失败,需通过xcargs注入版本。macOS App Store要求每个二进制文件签名,推荐通过DMG+公证分发。版本号由MARKETING_VERSION和CURRENT_PROJECT_VERSION组成,构建号每次上传必须递增。

4. Desktop原生安装包打包

Compose Multiplatform Gradle插件基于jpackage和jlink生成自包含安装包。支持的格式:macOS的.dmg/.pkg,Windows的.msi/.exe,Linux的.deb/.rpm。jpackage不支持交叉编译 ,打包需要JDK 17+。JDK版本必须跨平台统一。macOS的.dmg需要代码签名和公证。jDeploy作为替代方案,可自动生成GitHub Actions工作流。

5. Web(Wasm)静态托管部署

通过wasmJsBrowserDistribution任务构建,产物位于build/dist/wasmJs/productionExecutable/。部署关键:SPA重写规则 (所有路径重写到index.html)、.wasm文件的MIME类型配置为application/wasm、.wasm文件长期缓存、webApp.js每次检查更新。Cloudflare Pages从tag部署时需指定branch: main。

6. CI/CD流水线与版本管理

三阶段发布流程:Cut (手动创建prod分支)→ Deploy RC (推送到prod/*自动触发,打RC标签并上传)→ Tag Final (QA通过后手动打最终标签)。JetBrains官方推荐创建复合Action 统一Java和Gradle设置。大多数CMP项目首选GitHub Actions。签名密钥通过GitHub Secrets管理,keystore文件通过Base64编码传输。CI环境中禁用Gradle daemon并启用并行执行和缓存。Detekt在KMP项目中需配置源集特定任务,否则静默跳过共享代码。多平台版本号通过脚本统一计算和同步。

下一课预告:第20课将进入综合实践模块,深度解析CMP跨平台经典示例------包括JetBrains官方示例(Imageviewer、Chat、Nav Cupcake等)的架构决策,社区标杆项目(kmp-clean-architecture、MVI架构示例)的设计模式,支付宝MYKMP的工业实践,以及从示例中提炼CMP工程决策方法论。

相关推荐
红尘伴蝶舞2 小时前
加密拦截器让抓包失效,我只能从 4KB 的 logcat 碎片里拼回 JSON
android·kotlin·ai编程
booboosui2 小时前
219 个还是 0 个:读不到微信界面时,我只加不删
android
JMchen2 小时前
【Jetpack 内核实战 08 】:组件整合——DataStore + Room 构建离线优先架构
架构·kotlin·android jetpack
Android打工仔2 小时前
Compose 中何时该用 padding,何时该用 offset
android·android jetpack
二流小码农2 小时前
HarmonyOS开发助手,0代码让元服务从开发到上架
android·ios·harmonyos
limit00752 小时前
三维 WebGIS 工具选型:加载、样式、分析、交付,一套界面能做完什么
android·前端·web3·安卓·webgl
骑着蜗牛撵大象3273 小时前
让 Codex 值夜班:把告警排查与巡检接成可回滚的自动化链路
android·java·运维·ai·自动化·codex·智能体
2401_885885044 小时前
芬兰语音通知接口怎样对接?语音提醒接入流程
android·前端·python·php·语音识别
程序猿老A4 小时前
轻量应用服务器怎么用phpcms搭建网站?
android