为什么要学习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"
相关推荐
阿豪元代码20 分钟前
深入理解 SurfaceFlinger —— 如何调试 SurfaceFlinger
android
阿豪元代码26 分钟前
深入理解 SurfaceFlinger —— 概述
android
zeqinjie1 小时前
回顾 24年 Flutter 骨架屏没有释放 CurvedAnimation 导致内存泄漏的血案
前端·flutter·ios
CV资深专家2 小时前
Launcher3启动
android
stevenzqzq2 小时前
glide缓存策略和缓存命中
android·缓存·glide
雅雅姐3 小时前
Android 16 的用户和用户组定义
android
没有了遇见3 小时前
Android ConstraintLayout 之ConstraintSet
android
余辉zmh3 小时前
【MySQL基础篇】:MySQL索引——提升数据库查询性能的关键
android·数据库·mysql
tangweiguo030519874 小时前
Flutter Provider 状态管理全面解析与实战应用:从入门到精通
flutter
BennuCTech5 小时前
Google ML Kit系列:在Android上实现OCR本地识别
android