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

0 阅读20分钟

第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。

目录

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

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

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

平台分发渠道产物格式核心约束成熟度
AndroidGoogle Play / 国内应用商店APK / AABkeystore签名,versionCode递增Stable
iOSApp Store / TestFlightIPA必须在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目标为BetaBeta

三个关键跨平台约束:

桌面端不支持交叉编译。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生成:

keytool -genkey -v -keystore my-release-key.keystore \
  -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

2.2 配置签名

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

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,减小下载体积。

# 构建Release AAB
./gradlew :androidApp:bundleRelease

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

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

2.4 本地验证AAB

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

# 从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密钥:

# 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进行认证:

# 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注入版本属性:

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或更高版本。

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 / .pkgTargetFormat.Dmg / TargetFormat.Pkg
Windows.msi / .exeTargetFormat.Msi / TargetFormat.Exe
Linux.deb / .rpmTargetFormat.Deb / TargetFormat.Rpm

4.2 构建命令

# 构建当前操作系统的原生安装包
./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),否则用户打开时会看到安全警告:

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任务构建:

./gradlew :webApp:wasmJsBrowserDistribution

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

5.2 部署到静态托管

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

在Vercel中,通过vercel.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

# .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:

- 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 FinalQA通过后手动触发在prod分支打最终v{version}标签,创建GitHub Release

6.3 完整的GitHub Actions工作流

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

# .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
# .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矩阵

JobRunner执行内容
test-sharedubuntu-latestcommonTest + jvmTest(单元测试 + Compose UI测试)
test-androidubuntu-latestandroidHostTest(Roborazzi截图测试)
test-iosmacos-latestiosSimulatorArm64Test + Xcode UI Tests
build-androidubuntu-latestandroidApp:bundleRelease
build-iosmacos-latestXcode archive + TestFlight
build-webubuntu-latestwasmJsBrowserDistribution + 静态托管部署

6.5 版本管理策略

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

平台版本号位置说明
Androidbuild.gradle.kts的versionName / versionCodeversionCode必须递增
iOSConfig.xcconfig的MARKETING_VERSION / CURRENT_PROJECT_VERSION构建号必须递增
DesktopnativeDistributions.packageVersion语义化版本号
Web无强制版本要求可通过构建号标记

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

6.6 签名与密钥管理

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

Secret名称说明
KEYSTORE_BASE64Base64编码的keystore文件
KEYSTORE_PASSWORDkeystore密码
KEYSTORE_ALIASkeystore别名
KEY_PASSWORD密钥密码
PLAYSTORE_CREDSGoogle Play服务账号JSON

iOS签名需要:

Secret名称说明
ASC_API_KEYApp Store Connect API Key(JSON)
APPLE_IDApple ID(用于公证)
APPLE_APP_PASSWORDApp专用密码

安全实践: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工程决策方法论。