为什么要学习Flutter编译过程

1. 为什么要了解编译过程

  • 因为只有了解了flutter的编译过程,才能更好的做flutter CI CD工作。尤其是解决"如何让flutter开发适配目前已有的完备的iOS/Android的开发工作流"问题, 尤为重要。(如果是个人开发者可以忽略)。
  • 其次你会发现一个现象:"debug包 在离开xcode环境,脱机运行的时候,在初始化Flutter engine会失败。但是在xcode环境下运行debug包一切正常"。这就带来一个问题:有些业务需要再debug模式下提测,但提交给测试工程师的debug包flutter初始化会失败。为了解决这个问题,你需要了解flutter的编译过程来解决这个问题。

2. 编译过程

编译过程可以分为:

  • 编译前:为编译做准备,工程参数设定,脚本注入等
  • 编译:将flutter module编译为framework,并打包资源文件
  • 编译后:将打包后的framework和资源复制到相应的文件夹

我们会举例说明三个过程

2.1. Demo环境:目录结构和Podfile

2.1.1. 目录结构

bash 复制代码
/path/to/MyApp
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
└── MyApp/
    └── Podfile

2.1.2. Podfile

arduino 复制代码
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

MyApp/Podfile
target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end

post_install do |installer|
  flutter_post_install(installer) if defined?(flutter_post_install)
end

重点看podhelper.rb文件内的install_all_flutter_pods和flutter_post_install两个函数做了什么

2.2. 编译器前

编译前可以理解为pod install所做的事情,主要由install_all_flutter_pods和flutter_post_install两个函数完成

2.2.1. install_all_flutter_pods

ruby 复制代码
def install_all_flutter_pods(flutter_application_path = nil)
  # defined_in_file is a Pathname to the Podfile set by CocoaPods.
  pod_contents = File.read(defined_in_file)
  unless pod_contents.include? 'flutter_post_install'
    puts  <<~POSTINSTALL
Add `flutter_post_install(installer)` to your Podfile `post_install` block to build Flutter plugins:

post_install do |installer|
  flutter_post_install(installer)
end
POSTINSTALL
    raise 'Missing `flutter_post_install(installer)` in Podfile `post_install` block'
  end

  flutter_application_path ||= File.join('..', '..')
  install_flutter_engine_pod(flutter_application_path)
  install_flutter_plugin_pods(flutter_application_path)
  install_flutter_application_pod(flutter_application_path)
end

这里主要调用了三个函数:

  • install_flutter_engine_pod
  • install_flutter_plugin_pods
  • install_flutter_application_pod

这里重点看:install_flutter_application_pod。它为MyApp工程文件导入两个脚本:

  • Run Flutter Build my_flutter Script:根据环境编译flutter 和 App framework
  • Embed Flutter Build my_flutter Script:把编译后的产出复制到相应的目录
ruby 复制代码
def install_flutter_application_pod(flutter_application_path)
  flutter_application_path ||= File.join('..', '..')

  export_script_directory = File.join(flutter_application_path, '.ios', 'Flutter')

  # Keep script phase paths relative so they can be checked into source control.
  relative = flutter_relative_path_from_podfile(export_script_directory)

  flutter_export_environment_path = File.join('${SRCROOT}', relative, 'flutter_export_environment.sh')

  # Compile App.framework and move it and Flutter.framework to "BUILT_PRODUCTS_DIR"
  script_phase name: 'Run Flutter Build my_flutter Script',
    script: "set -e\nset -u\nsource "#{flutter_export_environment_path}"\nexport VERBOSE_SCRIPT_LOGGING=1 && "$FLUTTER_ROOT"/packages/flutter_tools/bin/xcode_backend.sh build",
    execution_position: :before_compile

  # Embed App.framework AND Flutter.framework.
  script_phase name: 'Embed Flutter Build my_flutter Script',
    script: "set -e\nset -u\nsource "#{flutter_export_environment_path}"\nexport VERBOSE_SCRIPT_LOGGING=1 && "$FLUTTER_ROOT"/packages/flutter_tools/bin/xcode_backend.sh embed_and_thin",
    execution_position: :after_compile
end

2.2.2. flutter_post_install

vbnet 复制代码
def flutter_post_install(installer, skip: false)
  return if skip

  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |_build_configuration|
      # flutter_additional_ios_build_settings is in Flutter root podhelper.rb
      flutter_additional_ios_build_settings(target)
    end
  end
end

进一步调用了flutter_additional_ios_build_settings,它的主要做用是根据当前环境设置MyApp的编译配置

为每个编译配置。比如framework搜索路径等

  • flutter_additional_ios_build_settings所在的文件:flutter_root/package/flutter_tools/bin/podhelper.rb
ruby 复制代码
def flutter_additional_ios_build_settings(target)
  return unless target.platform_name == :ios

  # [target.deployment_target] is a [String] formatted as "8.0".
  inherit_deployment_target = target.deployment_target[/\d+/].to_i < 12

  # ARC code targeting iOS 8 does not build on Xcode 14.3.
  force_to_arc_supported_min = target.deployment_target[/\d+/].to_i < 9

  # This podhelper script is at $FLUTTER_ROOT/packages/flutter_tools/bin.
  # Add search paths from $FLUTTER_ROOT/bin/cache/artifacts/engine.
  artifacts_dir = File.join('..', '..', '..', '..', 'bin', 'cache', 'artifacts', 'engine')
  debug_framework_dir = File.expand_path(File.join(artifacts_dir, 'ios', 'Flutter.xcframework'), __FILE__)

  unless Dir.exist?(debug_framework_dir)
    # iOS artifacts have not been downloaded.
    raise "#{debug_framework_dir} must exist. If you're running pod install manually, make sure "flutter precache --ios" is executed first"
  end

  release_framework_dir = File.expand_path(File.join(artifacts_dir, 'ios-release', 'Flutter.xcframework'), __FILE__)
  # Bundles are com.apple.product-type.bundle, frameworks are com.apple.product-type.framework.
  target_is_resource_bundle = target.respond_to?(:product_type) && target.product_type == 'com.apple.product-type.bundle'

  target.build_configurations.each do |build_configuration|
    # Build both x86_64 and arm64 simulator archs for all dependencies. If a single plugin does not support arm64 simulators,
    # the app and all frameworks will fall back to x86_64. Unfortunately that case is not detectable in this script.
    # Therefore all pods must have a x86_64 slice available, or linking a x86_64 app will fail.
    build_configuration.build_settings['ONLY_ACTIVE_ARCH'] = 'NO' if build_configuration.type == :debug

    # Workaround https://github.com/CocoaPods/CocoaPods/issues/11402, do not sign resource bundles.
    if target_is_resource_bundle
      build_configuration.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
      build_configuration.build_settings['CODE_SIGNING_REQUIRED'] = 'NO'
      build_configuration.build_settings['CODE_SIGNING_IDENTITY'] = '-'
      build_configuration.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = '-'
    end

    # ARC code targeting iOS 8 does not build on Xcode 14.3. Force to at least iOS 9.
    build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0' if force_to_arc_supported_min

    # Skip other updates if it does not depend on Flutter (including transitive dependency)
    next unless depends_on_flutter(target, 'Flutter')

    # Bitcode is deprecated, Flutter.framework bitcode blob will have been stripped.
    build_configuration.build_settings['ENABLE_BITCODE'] = 'NO'

    # Profile can't be derived from the CocoaPods build configuration. Use release framework (for linking only).
    # TODO(stuartmorgan): Handle local engines here; see https://github.com/flutter/flutter/issues/132228
    configuration_engine_dir = build_configuration.type == :debug ? debug_framework_dir : release_framework_dir
    Dir.new(configuration_engine_dir).each_child do |xcframework_file|
      next if xcframework_file.start_with?('.') # Hidden file, possibly on external disk.
      if xcframework_file.end_with?('-simulator') # ios-arm64_x86_64-simulator
        build_configuration.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]'] = ""#{configuration_engine_dir}/#{xcframework_file}" $(inherited)"
      elsif xcframework_file.start_with?('ios-') # ios-arm64
        build_configuration.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]'] = ""#{configuration_engine_dir}/#{xcframework_file}" $(inherited)"
       # else Info.plist or another platform.
      end
    end
    build_configuration.build_settings['OTHER_LDFLAGS'] = '$(inherited) -framework Flutter'

    build_configuration.build_settings['CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER'] = 'NO'
    # Suppress warning when pod supports a version lower than the minimum supported by Xcode (Xcode 12 - iOS 9).
    # This warning is harmless but confusing--it's not a bad thing for dependencies to support a lower version.
    # When deleted, the deployment version will inherit from the higher version derived from the 'Runner' target.
    # If the pod only supports a higher version, do not delete to correctly produce an error.
    build_configuration.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' if inherit_deployment_target

    # Override legacy Xcode 11 style VALID_ARCHS[sdk=iphonesimulator*]=x86_64 and prefer Xcode 12 EXCLUDED_ARCHS.
    build_configuration.build_settings['VALID_ARCHS[sdk=iphonesimulator*]'] = '$(ARCHS_STANDARD)'
    build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = '$(inherited) i386'
    build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphoneos*]'] = '$(inherited) armv7'
  end
end

2.3. 编译

当MyApp工程开始编译,就胡已调用'Run Flutter Build my_flutter Script'脚本编译flutter module。

bash 复制代码
set -e
set -u
source "${SRCROOT}/../my_flutter/.ios/Flutter/flutter_export_environment.sh"
export VERBOSE_SCRIPT_LOGGING=1 && "$FLUTTER_ROOT"/packages/flutter_tools/bin/xcode_backend.sh build

这里还加载了一个文件flutter_export_environment.sh。添加了一些环境变量,这些环境变量可以控制编译flutter module的参数。

bash 复制代码
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/Users/yidao/Documents/env/1/flutter"
export "FLUTTER_APPLICATION_PATH=/Users/yidao/Documents/code/flutter/my_flutter"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=.dart_tool/package_config.json"

# debug profile release
#export "FLUTTER_BUILD_MODE=release"

xcode_backend.sh其实是一个转发脚本到xcode_backend.sh build

bash 复制代码
#!/usr/bin/env bash
# Copyright 2014 The Flutter Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# exit on error, or usage of unset var
set -euo pipefail

# Needed because if it is set, cd may print the path it changed to.
unset CDPATH

function follow_links() (
  cd -P "$(dirname -- "$1")"
  file="$PWD/$(basename -- "$1")"
  while [[ -h "$file" ]]; do
    cd -P "$(dirname -- "$file")"
    file="$(readlink -- "$file")"
    cd -P "$(dirname -- "$file")"
    file="$PWD/$(basename -- "$file")"
  done
  echo "$file"
)

PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"
BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
FLUTTER_ROOT="$BIN_DIR/../../.."
DART="$FLUTTER_ROOT/bin/dart"

"$DART" "$BIN_DIR/xcode_backend.dart" "$@"

xcode_backend.sh进一步调用了同文件夹下的xcode_backend.dart build。看xcode_backend关键代码:

javascript 复制代码
void main(List<String> arguments) {
  File? scriptOutputStreamFile;
  final String? scriptOutputStreamFileEnv = Platform.environment['SCRIPT_OUTPUT_STREAM_FILE'];
  if (scriptOutputStreamFileEnv != null && scriptOutputStreamFileEnv.isNotEmpty) {
    scriptOutputStreamFile = File(scriptOutputStreamFileEnv);
  }
  Context(
    arguments: arguments,
    environment: Platform.environment,
    scriptOutputStreamFile: scriptOutputStreamFile,
  ).run();
}

class Context {
  Context({required this.arguments, required this.environment, File? scriptOutputStreamFile}) {
    if (scriptOutputStreamFile != null) {
      scriptOutputStream = scriptOutputStreamFile.openSync(mode: FileMode.write);
    }
  }

  final Map<String, String> environment;
  final List<String> arguments;
  RandomAccessFile? scriptOutputStream;

  void run() {
    if (arguments.isEmpty) {
      // Named entry points were introduced in Flutter v0.0.7.
      stderr.write(
        'error: Your Xcode project is incompatible with this version of Flutter. '
        'Run "rm -rf ios/Runner.xcodeproj" and "flutter create ." to regenerate.\n',
      );
      exit(-1);
    }

    final String subCommand = arguments.first;
    switch (subCommand) {
      case 'build':
        buildApp(); //>>>>>>>>>>>>>>>看这里<<<<<<<<<<<
      case 'prepare':
        prepare();
      case 'thin':
        // No-op, thinning is handled during the bundle asset assemble build target.
        break;
      case 'embed':
        embedFlutterFrameworks();
      case 'embed_and_thin':
        // Thinning is handled during the bundle asset assemble build target, so just embed.
        embedFlutterFrameworks();
      case 'test_vm_service_bonjour_service':
        // Exposed for integration testing only.
        addVmServiceBonjourService();
    }
  }
}
  • main函数调用Context.run
  • run通过判断传参为build,然后调用buildApp()方法
dart 复制代码
  void buildApp() {
    final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
    final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
    final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';

    final String buildMode = parseFlutterBuildMode();

    final List<String> flutterArgs = _generateFlutterArgsForAssemble('build', buildMode, verbose);

    flutterArgs.add('${buildMode}_ios_bundle_flutter_assets');

    final ProcessResult result = runSync(
      '${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
      flutterArgs,
      verbose: verbose,
      allowFail: true,
      workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
    );

    if (result.exitCode != 0) {
      echoError('Failed to package $projectPath.');
      exitApp(-1);
    }

    streamOutput('done');
    streamOutput(' └─Compiling, linking and signing...');

    echo('Project $projectPath built and packaged successfully.');
  }

buildApp完成:

  • 基于环境准备编译参数
  • 通过runSync函数编译flutter module

重点看一下对parseFlutterBuildMode();的调用。

  • 它会返回当前的编译模式:debug、profile、release

编译模式从两个环境变量获取:

  • environment['FLUTTER_BUILD_MODE']:优先使用此变量。可以在上面提到的flutter_export_environment.sh中设置,也可以通过其他途径设置。
  • environment['CONFIGURATION']:如果上面变量没有设置,则使用此变量。此变量是Xcode从工程文件设置中读取并设置此变量。
  • 总结:如果没有设置FLUTTER_BUILD_MODE,则按照xcode当前编译设置选择编译模式。这样就说xcode编译debug包,则flutter module也编译debug包,如果是.......
javascript 复制代码
  String parseFlutterBuildMode() {
    // Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
    // This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
    // they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
    final String? buildMode =
        (environment['FLUTTER_BUILD_MODE'] ?? environment['CONFIGURATION'])?.toLowerCase();

    if (buildMode != null) {
      if (buildMode.contains('release')) {
        return 'release';
      }
      if (buildMode.contains('profile')) {
        return 'profile';
      }
      if (buildMode.contains('debug')) {
        return 'debug';
      }
    }
  }

2.4. 编译后

通过编译前注入的"Embed Flutter Build my_flutter Script"脚本,完成Embed Flutter Build

bash 复制代码
set -e
set -u
source "${SRCROOT}/../my_flutter/.ios/Flutter/flutter_export_environment.sh"
export VERBOSE_SCRIPT_LOGGING=1 && "$FLUTTER_ROOT"/packages/flutter_tools/bin/xcode_backend.sh embed_and_thin

这个阶段的职责:

操作 说明
嵌入 Flutter.framework 和 App.framework 确保它们包含在最终 App 中
瘦身处理(thin) 移除不必要的架构,提高打包效率
复制资源 包括 Dart AOT、Asset、VM Snapshot 等运行时资源

和编译脚本同样也会调用Context的run函数:

javascript 复制代码
void run() {
  if (arguments.isEmpty) {
    // Named entry points were introduced in Flutter v0.0.7.
    stderr.write(
      'error: Your Xcode project is incompatible with this version of Flutter. '
      'Run "rm -rf ios/Runner.xcodeproj" and "flutter create ." to regenerate.\n',
    );
    exit(-1);
  }

  final String subCommand = arguments.first;
  switch (subCommand) {
    case 'build':
      buildApp();
    case 'prepare':
      prepare();
    case 'thin':
      // No-op, thinning is handled during the bundle asset assemble build target.
      break;
    case 'embed':
      embedFlutterFrameworks();
    case 'embed_and_thin':
      // Thinning is handled during the bundle asset assemble build target, so just embed.
      embedFlutterFrameworks();
    case 'test_vm_service_bonjour_service':
      // Exposed for integration testing only.
      addVmServiceBonjourService();
  }
}

这里调用了embedFlutterFrameworks函数:

dart 复制代码
  void embedFlutterFrameworks() {
    // Embed App.framework from Flutter into the app (after creating the Frameworks directory
    // if it doesn't already exist).
    final String xcodeFrameworksDir =
        '${environment['TARGET_BUILD_DIR']}/${environment['FRAMEWORKS_FOLDER_PATH']}';
    runSync('mkdir', <String>['-p', '--', xcodeFrameworksDir]);
    runRsync(
      delete: true,
      '${environment['BUILT_PRODUCTS_DIR']}/App.framework',
      xcodeFrameworksDir,
    );

    // Embed the actual Flutter.framework that the Flutter app expects to run against,
    // which could be a local build or an arch/type specific build.
    runRsync(
      delete: true,
      '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework',
      '$xcodeFrameworksDir/',
    );

    // Copy the native assets. These do not have to be codesigned here because,
    // they are already codesigned in buildNativeAssetsMacOS.
    final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
    String projectPath = '$sourceRoot/..';
    if (environment['FLUTTER_APPLICATION_PATH'] != null) {
      projectPath = environment['FLUTTER_APPLICATION_PATH']!;
    }
    final String flutterBuildDir = environment['FLUTTER_BUILD_DIR']!;
    final String nativeAssetsPath = '$projectPath/$flutterBuildDir/native_assets/ios/';
    final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
    if (Directory(nativeAssetsPath).existsSync()) {
      if (verbose) {
        print('♦ Copying native assets from $nativeAssetsPath.');
      }
      runRsync(
        extraArgs: <String>['--filter', '- native_assets.yaml', '--filter', '- native_assets.json'],
        nativeAssetsPath,
        xcodeFrameworksDir,
      );
    } else if (verbose) {
      print("♦ No native assets to bundle. $nativeAssetsPath doesn't exist.");
    }

    addVmServiceBonjourService();
  }

至此flutter module的编译完成

3. 解决debug包提测问题

3.1. 问题:

  • 有些业务需要再debug模式下提测,但提交给测试工程师的debug包flutter初始化会失败。

3.2. 原因:

3.2.1. 分析

  • Debug 模式的 Flutter Add-to-App 项目中,flutter module 会被编译为debug包。FlutterEngine 启动时会尝试 连接开发主机上的 flutter attach 服务,用于:
    • 调试功能(热重载、日志打印、DevTools)
    • 加载 Dart VM snapshot 等临时调试资源
  • 而这些功能只有在 Xcode 启动时会设置好相关参数路径(通过环境变量或启动参数传入)

3.2.2. 所以:

  • ✅ Xcode 启动时,会自动设置 --observatory-port 等参数,FlutterEngine 知道去哪找 Dart runtime
  • ❌ 图标点击启动时,这些参数没有传入 ,FlutterEngine 找不到调试环境、资源路径,导致 run 返回 NO(Dart isolate 无法启动)

3.3. 如何解决

  • 思路:在debug App包中中使用flutter module的profile或release包。profile或release包不依赖xcode提供的环境。但也不能hot reload dart代码。
  • 实现:可以通过设置上面提到的"environment['FLUTTER_BUILD_MODE']"环境变量,控制flutter module的编译模式
bash 复制代码
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/Users/yidao/Documents/env/1/flutter"
export "FLUTTER_APPLICATION_PATH=/Users/yidao/Documents/code/flutter/my_flutter"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=.dart_tool/package_config.json"

# debug profile release
export "FLUTTER_BUILD_MODE=release"
相关推荐
程序员JerrySUN6 小时前
Valgrind Memcheck 全解析教程:6个程序说明基础内存错误
android·java·linux·运维·开发语言·学习
经典19927 小时前
mysql 性能优化之Explain讲解
android·mysql·性能优化
Kiri霧9 小时前
Kotlin集合与空值
android·开发语言·kotlin
Glacien10 小时前
compose动画从底层基础到顶层高级应用(三)核心API之--Transition
android
suqingxiao11 小时前
android虚拟机(AVD)报错The emulator process for AVD xxx has terminated
android
whysqwhw11 小时前
OkHttp Cookie 处理机制全解析
android
Evan_ZGYF丶11 小时前
【RK3576】【Android14】ADB工具说明与使用
android·驱动开发·android14·rk3576
幻雨様11 小时前
UE5多人MOBA+GAS 番外篇:移植Lyra的伤害特效(没用GameplayCue,因为我失败了┭┮﹏┭┮)
android·ue5
狂浪天涯12 小时前
Android 16 显示系统 | 从View 到屏幕系列 - 4 | GraphicBuffer & Gralloc
android·操作系统