跳转至

多环境支持

搭建多环境支持,基于同一套源代码自动构建出有差异功能的app,分离测试和生产环境。

Xcode 构建基础概念

在构建一个 iOS App 的时候,需要用到 Xcode Project,Xcode Target,Build Settings,Build Configuration 和 Xcode Scheme 等构建配置。

Xcode Project

Xcode Project用于组织源代码文件和资源文件。一个 Project 可以包含多个 Target,当新建一个 Xcode Project 的时候,会自动生成主 TargetUnit Test TargetUI Test Target

多target

动态库或iWatch的应用的BundleID必须要跟主应用程序的bundleid一致,比如你的应用的id是com.mycop.hello,那么内嵌的id必须是com.mycop.hello.xxx

xcconfig

创建Configurations目录,创建xcconfig文件

主工程创建Configuration Setting File文件,xcconfig是控制build setting的。

编译需要配置:

PROJECT->Info->Configurations->Debug和Release->每一个target对应一个xcconfig文件

只能配置一个config文件,如果有其它的config文件需要include。

配置了xcconfig,要在对应的build setting中对应设置的值给删除,改为设置$(inherited)继承。

使用Build Configuration 和 Xcode Scheme来管理多环境

在构建过程中使用Xcode Scheme选择不同的Build Configuration

Xcode Scheme

Xcode Scheme用于定义一个完整的构建过程,包括指定哪些 Target 需要进行构建,构建过程中使用了哪个 Build Configuration ,以及需要执行哪些测试案例等等。可以为一个项目建立多个 Scheme,但同一时刻只能有一个 Scheme 生效。

环境变量

OBJC_PRINT_LOAD_METHODS:哪些类实现了load方法。

fastlane自动化打包

  • 安装 Xcode command line tools:xcode-select --install
  • 安装fastlane:brew install fastlane
  • cd工程所在目录,执行fastlane init
  • 选择2打包testfilght。
  • 登录开发者账号、用户和访问、密钥,App Store Connect API,获取Issuer ID、密钥id,下载密钥p8文件、拷贝到fastlane文件夹。

fastlane [lane_name]

执行:fastlane [name of the lane]或者bundle exec fastlane [name of the lane]

Gym常用配置项:

Name Type Description Default
scheme string 指定需要编译的scheme
clean bool 是否在编译前clean false
output_directory string 导出目录 ./
output_name string 导出ipa名字 [app_name].ipa
export_options hash/string 这里指定Xcode API的外部配置文件地址,或者配置hash,见下文
export_method string 打包方式,可选项app-store ad-hoc package``enterprise``development``developer-id 如果在fastlane中使用了sigh,这个值会从上下文获取
include_bitcode bool 是否开启bitcode Xcode API 默认值为true
include_symbols bool 是否生成符号表 Xcode API 默认值为true

Xcode API允许我们指定一个plist文件作为额外的配置文件。gym默认会帮你创建这个文件,你可以直接指定配置。更多关于plist可配置项,执行xcodebuild -help查看Available keys for -exportOptionsPlist

export_methodinclude_symbols,和include_bitcode 这些参数都是exportOptionsPlist的配置,对应methoduploadSymbolsuploadBitcode

Gym可以指定配置文件Gymfile

初始化:

gym init

Fastfile

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#     https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#     https://docs.fastlane.tools/plugins/available-plugins
#

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane

default_platform(:ios)

platform :ios do

  desc "Push a new beta build to TestFlight"
  lane :beta do
    increment_build_number(xcodeproj: "SCM.xcodeproj")
    # gym(
    #   # 每次打包之前clean一下
    #   clean: true,
    #   workspace: "SCM.xcworkspace",
    #   scheme: "SCM",
    #   output_directory: './fastlane/release',
    #   output_name: 'SCM.ipa',
    #   configuration: 'Release',
    #   include_bitcode: true,
    #   include_symbols: true,
    #   # app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application
    #   export_method: 'app-store',
    #   export_xcargs: '-allowProvisioningUpdates'
    # )

    build_app(
      # 每次打包之前clean一下
      clean: true,
      workspace: "SCM.xcworkspace",
      scheme: "SCM",
      output_directory: './fastlane/release',
      output_name: 'SCM.ipa',
      configuration: 'Release',
      include_bitcode: false,
      include_symbols: true,
      # app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application
      export_method: 'app-store',
      export_xcargs: '-allowProvisioningUpdates'
    )

    # mac上的通知弹窗,通知打包完毕
    notification(app_icon: './fastlane/icon.png', title: '打包成功', subtitle: '打包成功,已导出安装包', message: '准备发布中……')

    # 上传到testflight
    # upload_to_testflight和(之间不要有空格
    # upload_to_testflight(
    #     # 上边设置的授权信息
    #     api_key: get_app_store_connect_api_key,
    #     skip_waiting_for_build_processing: true,
    #     # 打包好要上传的文件
    #     ipa: './fastlane/release/SCM.ipa',
    #     skip_submission: true
    # )

    upload_to_app_store(
      # 上边设置的授权信息
      api_key: get_app_store_connect_api_key,
      # 打包好要上传的文件
      ipa: './fastlane/release/SCM.ipa',
      app_identifier: "com.ningmengyun.SCMTest",
      # skip_waiting_for_build_processing: true,
      # skip_submission: true,
      skip_metadata: true,
      skip_screenshots: true,
      precheck_include_in_app_purchases: false,
    )

    # 通知上传成功
    notification(app_icon:"icon.png",title:"上传成功",subtitle: "IPA上传成功", message: "自动打包完成!")
  end

  # 配置上传到App Store connect的api_key
  # 通过这种设置可以绕过二次认证等验证操作,实现一键打包上传
  desc 'Get App Store Connect API key'
  private_lane :get_app_store_connect_api_key do
    # The key needs to have App Manager role, see https://github.com/fastlane/fastlane/issues/17066
    key_content = ENV["APP_STORE_CONNECT_API_CONTENT"]  # Make sure setting this environment variable before call this lane.
    api_key = app_store_connect_api_key(
      # 通过苹果申请的key id,申请同时会生成issuer_id和一个.p8授权文件,就是以下参数,文件只能下载一次,注意保存,申请方式https://appleid.apple.com/account/manage中的专用密钥
      key_id: '9B6ASXN3GN',
      issuer_id: '69a6de8f-0a10-47e3-e053-5b8c7c11a4d1',
      # 授权文件路径
      key_filepath: './fastlane/AuthKey_9B6ASXN3GN.p8',
      # key_content: "-----BEGIN EC PRIVATE KEY-----\n" + key_content + "\n-----END EC PRIVATE KEY-----",
      duration: 1200,
      in_house: false
    )
    api_key 
  end

end

🛠️ 环境准备:三步搭建基础环境

第一步:安装Fastlane

# 使用RubyGems安装(推荐)sudo gem install fastlane -NV
# 或使用Homebrewbrew install fastlane

第三步:验证安装

fastlane --version

🚀 Fastlane初始化:五分钟快速上手

进入你的Flutter项目iOS目录,开始初始化:

cd ios
fastlane init

初始化过程中,选择第4项"手动设置",然后输入你的Apple ID信息。

初始化成功后,会生成 fastlane 目录。

图片

.ENV 文件手动创建一下,用来保存开发者的密钥

# Apple 
IDAPPLE_ID=test@qq.com
# App Specific Password (https://appleid.apple.com/account/manage)
# Used by Transporter for binary upload (but not for logging into App Store Connect API)
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD= 专用密码
# App Store Connect API Key (Recommended for 2FA/Auth issues)
# Create one at: https://appstoreconnect.apple.com/access/api
APP_STORE_CONNECT_API_KEY_KEY_ID=
APP_STORE_CONNECT_API_KEY_ISSUER_ID=
# Paste the content of the .p8 file here, replacing newlines with \n if needed, or just the raw content
APP_STORE_CONNECT_API_KEY_CONTENT="-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----"

App 专用密码 (推荐用于自动上传)

获取方式:登录 appleid.apple.com -> App 专用密码 -> 生成一个

🔧 Appfile核心配置文件详解

# ios/fastlane/Appfile
app_identifier("com.yourcompany.appname")  # 你的Bundle ID
apple_id("your.email@example.com")         # Apple ID
team_id("ABCD123456")                      # 开发者团队ID
# For more information about the Appfile, see:
#     https://docs.fastlane.tools/advanced/#appfile

Fastfile - 自动化流程的核心

default_platform(:ios)
# =============================================================================
#  辅助工具方法
# =============================================================================
# ⏱️ 耗时统计函数
# 用于精确记录每个构建步骤所花费的时间(分:秒),方便性能分析和优化。
def measure_time(step_name)
  start_time = Time.now
  UI.message("⏱️  [#{step_name}] 开始时间: #{start_time.strftime('%H:%M:%S')}")

  yield # 执行传入的代码块

  end_time = Time.now
  duration = end_time - start_time
  minutes = (duration / 60).to_i
  seconds = (duration % 60).to_i

  UI.success("✅ [#{step_name}] 完成时间: #{end_time.strftime('%H:%M:%S')} | 耗时: #{minutes}#{seconds}秒")
end
platform :ios do
  # =============================================================================
  #  初始化配置 (Before All)
  # =============================================================================

  # 在任何 Lane 开始执行之前运行。
  # 主要用于配置 App Store Connect API Key,这是解决双重认证(2FA)问题的最佳方案。
  # 密钥信息存储在 .env 文件中,避免硬编码,提高安全性。
  before_all do
    if ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'] && !ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'].empty?
      app_store_connect_api_key(
        key_id: ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'],
        issuer_id: ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID'],
        # 处理 .env 文件中可能存在的换行符格式问题,确保私钥内容正确
        key_content: ENV['APP_STORE_CONNECT_API_KEY_CONTENT'].to_s.gsub("\\n", "\n"),
        duration: 1200, # Token 有效期 (秒)
        in_house: false # 是否为企业账号 (Enterprise)
      )
    end
  end
  # =============================================================================
  #  Lane: beta (TestFlight 内测版)
  # =============================================================================

  desc "打包并上传到 TestFlight (内测)"
  lane :beta do
    # 1. 清理 Flutter 项目
    # 删除 build 目录,确保是一个干净的构建环境,避免缓存导致的问题。
    measure_time("Flutter Clean") do
      sh("cd ../.. && fvm flutter clean")
    end
    # 2. 构建 iOS 原生工程
    # 使用 release 模式构建,生成 App.framework 等产物。
    # --no-codesign: 此时不需要签名,签名在后续导出 IPA 时进行。
    # --no-tree-shake-icons: 防止字体图标被摇树优化掉(某些情况下会导致图标丢失)。
    measure_time("Flutter Build iOS") do
      sh("cd ../.. && fvm flutter build ios --release --no-codesign --no-tree-shake-icons")
    end
    # 3. 打包 IPA (Gym)
    # 使用 fastlane gym 插件将 Xcode 工程打包成 .ipa 文件。
    measure_time("Build IPA (Gym)") do
      build_app(
        workspace: "Runner.xcworkspace", # 指定工作空间
        scheme: "Runner",                # 指定 Scheme
        clean: true,                     # 打包前再次清理 Xcode 构建产物
        export_method: "app-store"       # 导出方式:app-store (用于上架或TestFlight)
      )
    end
    # 4. 上传到 TestFlight
    # 使用 fastlane pilot 插件上传 ipa 到 App Store Connect。
    measure_time("Upload to TestFlight") do
      upload_to_testflight(
        # 设置为 true 则上传成功后立即结束,不等待苹果后台处理完毕(处理通常需要几分钟)
        skip_waiting_for_build_processing: true 
      )
    end
  end
  # =============================================================================
  #  Lane: release (App Store 正式版)
  # =============================================================================

  desc "打包并上传到 App Store (正式发版)"
  lane :release do
    # 1. 清理
    measure_time("Flutter Clean") do
      sh("cd ../.. && fvm flutter clean")
    end
    # 2. 构建
    measure_time("Flutter Build iOS") do
      sh("cd ../.. && fvm flutter build ios --release --no-codesign --no-tree-shake-icons")
    end
    # 3. 打包 IPA
    measure_time("Build IPA (Gym)") do
      build_app(
        workspace: "Runner.xcworkspace",
        scheme: "Runner",
        clean: true,
        export_method: "app-store"
      )
    end
    # 4. 上传到 App Store
    # 使用 fastlane deliver 插件上传 ipa。
    measure_time("Upload to App Store") do
      upload_to_app_store(
        skip_metadata: true,    # 不上传元数据(标题、描述等),仅上传二进制
        skip_screenshots: true, # 不上传截图
        force: true             # 跳过 HTML 报告确认,直接上传
      )
    end
  end
end

Apple ID 开启了 双重认证 (2FA) ,而 Fastlane 在自动化脚本中无法处理这种交互式验证(或者 Session 过期了)。仅仅使用 App 专用密码 ( FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD ) 对于某些 Fastlane 操作(如登录 App Store Connect API 获取元数据)是不够的,它主要用于上传二进制文件。

最佳解决方案:使用 App Store Connect API Key

这是苹果官方推荐的自动化方案,完全绕过 2FA,稳定且安全。

1. 生成 API Key

可以参考以下步骤操作:

  1. 登录 App Store Connect 。
  2. 点击 "Users and Access" (用户和访问) -> "Integrations" (集成) 标签页。
  3. 选择 "App Store Connect API" 。
  4. 点击 "+" 生成一个新的 Key。

- Name : Fastlane (或者任意名字)

- Access : App Manager (App 管理)

  1. 下载 .p8 私钥文件( 注意:只能下载一次,请妥善保存 )。
  2. 记下页面上的 Issuer ID 和 Key ID 。

图片

图片

图片

2. 配置 .env 文件

打开 ios/fastlane/.env 文件,我已经为你预留了位置,请填入刚才获取的信息:

# ... (保留原有的 APPLE_ID 配置)

# App Store Connect API Key (推荐用于解决 Unauthorized Access 问题)
APP_STORE_CONNECT_API_KEY_KEY_ID=你的Key_ID      # 例如: D383SF739
APP_STORE_CONNECT_API_KEY_ISSUER_ID=你的Issuer_ID  # 例如: 69a6de78-xxxx-xxxx-xxxx-xxxxxxx
APP_STORE_CONNECT_API_KEY_CONTENT=你的p8文件内容   # 打开下载的 .p8 文件,复制全部内容粘贴到这里

注意 : APP_STORE_CONNECT_API_KEY_CONTENT 可以直接粘贴 .p8 文件的全部文本内容(包括 -----BEGIN PRIVATE KEY----- 等)。

3. 再次运行

配置完成后,Fastlane 会自动检测并使用 API Key 进行认证,应该就能解决 Unauthorized Access 报错了。

cd ios
bundle exec fastlane beta

最后上传成功。

图片