Flutter以framework集成入iOS项目方案

现状

Flutter官方的执行方案对Flutter工程及环境有很强的依赖性,非Flutter的成员在对iOS主工程进行迭代开发时需要依赖Flutter环境,团队合作十分不便。在通过Jenkins等打包时会有各种问题。

官方文章地址:Add Flutter to existing apps​

基于Flutter版本1.9.1+hotfix 4

适用于以flutter module进行开发,作为iOS一个组件来引入的情况

(可以直接跳至方案查看结果)

期望

flutter 集成进iOS项目脱离Flutter环境及工程,Flutter工程开发与iOS原生开发互不影响。

分析

Flutter执行build之后产物介绍

在Flutter的module中执行flutter build ios --release后,我们的工程目录里有隐藏文件夹.ios,我们需要的Flutter产物基本都在其下的Flutter文件夹中。

image-20191031164025401.png

挨个分析一下内部我们需要的文件。

  • .symlinks

    我们三方库的索引,内部每个文件夹下,都包含ios文件夹,这个文件夹下都是一个pod库。

  • App.framework

    Flutter项目的Dart代码编译而成的Framework。

  • engine

    Flutter的引擎Framework,一个pod库。

  • FlutterPluginRegistrant

    Flutter三方库的注册入口,一个pod库。

  • Generated.xcconfig

    Flutter的路径信息,配置信息等。

  • podhelper.rb

    pod执行的时候用到的脚本。后续会对这个文件做具体的分析

  • 其他的文件基本都没什么用处可以不关注

官方方案分析

官方执行步骤:
  1. Podfile中加入如下代码:
flutter_application_path = 'path/to/my_flutter/'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  1. 对每个Xcode target添加方法install_all_flutter_pods(flutter_application_path)
target 'MyApp' do
     install_all_flutter_pods(flutter_application_path)
end
target 'MyAppTests' do
     install_all_flutter_pods(flutter_application_path)
end

分析:

第一步作用是设置flutter工程路径并添加脚本podhelper.rb。

第二步作用就是执行podhelper.rb中的方法install_all_flutter_pods,并将flutter工程路径变量传入。

那么下面我们来分析一下这个podhelper.rb中到底做了什么。

分析podhelper.rb

脚本代码如下:

# Install pods needed to embed Flutter application, Flutter engine, and plugins
# from the host application Podfile.
#
# @example
#   target 'MyApp' do
#     install_all_flutter_pods 'my_flutter'
#   end
# @param [String] flutter_application_path Path of the root directory of the Flutter module.
#                                          Optional, defaults to two levels up from the directory of this script.
#                                          MyApp/my_flutter/.ios/Flutter/../..
def install_all_flutter_pods(flutter_application_path = nil)
  flutter_application_path ||= File.join('..', '..')
  install_flutter_engine_pod
#  install_flutter_plugin_pods(flutter_application_path)
  install_flutter_application_pod(flutter_application_path)
end

# Install Flutter engine pod.
#
# @example
#   target 'MyApp' do
#     install_flutter_engine_pod
#   end
def install_flutter_engine_pod
  engine_dir = File.join(__dir__, 'engine')
  if !File.exist?(engine_dir)
    # Copy the debug engine to have something to link against if the xcode backend script has not run yet.
    # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
    debug_framework_dir = File.join(flutter_root, 'bin', 'cache', 'artifacts', 'engine', 'ios')
    FileUtils.mkdir_p(engine_dir)
    FileUtils.cp_r(File.join(debug_framework_dir, 'Flutter.framework'), engine_dir)
    FileUtils.cp(File.join(debug_framework_dir, 'Flutter.podspec'), engine_dir)
  end

  # Keep pod path relative so it can be checked into Podfile.lock.
  # Process will be run from project directory.
  engine_pathname = Pathname.new engine_dir
  project_directory_pathname = Pathname.new Dir.pwd
  relative = engine_pathname.relative_path_from project_directory_pathname

  pod 'Flutter', :path => relative.to_s, :inhibit_warnings => true
end

# Install Flutter plugin pods.
#
# @example
#   target 'MyApp' do
#     install_flutter_plugin_pods 'my_flutter'
#   end
# @param [String] flutter_application_path Path of the root directory of the Flutter module.
#                                          Optional, defaults to two levels up from the directory of this script.
#                                          MyApp/my_flutter/.ios/Flutter/../..
#def install_flutter_plugin_pods(flutter_application_path)
#  flutter_application_path ||= File.join('..', '..')
#
#  # Keep pod path relative so it can be checked into Podfile.lock.
#  # Process will be run from project directory.
#  current_directory_pathname = Pathname.new __dir__
#  project_directory_pathname = Pathname.new Dir.pwd
#  relative = current_directory_pathname.relative_path_from project_directory_pathname
#  pod 'FlutterPluginRegistrant', :path => File.join(relative, 'FlutterPluginRegistrant'), :inhibit_warnings => true
#
#  symlinks_dir = File.join(relative, '.symlinks')
#  FileUtils.mkdir_p(symlinks_dir)
#  plugin_pods = parse_KV_file(File.join(flutter_application_path, '.flutter-plugins'))
#  plugin_pods.map do |r|
#    symlink = File.join(symlinks_dir, r[:name])
#    FileUtils.rm_f(symlink)
#    File.symlink(r[:path], symlink)
#    pod r[:name], :path => File.join(symlink, 'ios'), :inhibit_warnings => true
#  end
#end

# Install Flutter application pod.
#
# @example
#   target 'MyApp' do
#     install_flutter_application_pod '../flutter_settings_repository'
#   end
# @param [String] flutter_application_path Path of the root directory of the Flutter module.
#                                          Optional, defaults to two levels up from the directory of this script.
#                                          MyApp/my_flutter/.ios/Flutter/../..
def install_flutter_application_pod(flutter_application_path)
  app_framework_dir = File.join(__dir__, 'App.framework')
  app_framework_dylib = File.join(app_framework_dir, 'App')
  if !File.exist?(app_framework_dylib)
    # Fake an App.framework to have something to link against if the xcode backend script has not run yet.
    # CocoaPods will not embed the framework on pod install (before any build phases can run) if the dylib does not exist.
    # Create a dummy dylib.
    FileUtils.mkdir_p(app_framework_dir)
    `echo "static const int Moo = 88;" | xcrun clang -x c -dynamiclib -o "#{app_framework_dylib}" -`
  end

  # Keep pod and script phase paths relative so they can be checked into source control.
  # Process will be run from project directory.
  current_directory_pathname = Pathname.new __dir__
  project_directory_pathname = Pathname.new Dir.pwd
  relative = current_directory_pathname.relative_path_from project_directory_pathname
  pod 'flutter_module', :path => relative.to_s, :inhibit_warnings => true

#  flutter_export_environment_path = File.join('${SRCROOT}', relative, 'flutter_export_environment.sh');
#  script_phase :name => 'Run Flutter Build Script',
#  :script => "set -e\nset -u\nsource \"#{flutter_export_environment_path}\"\n../xcode_backend.sh build",
##  :script => "set -e\nset -u\nsource \"#{flutter_export_environment_path}\"\n\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/xcode_backend.sh build",
#
#  :input_files => [
#      File.join('${SRCROOT}', flutter_application_path, '.metadata'),
#      File.join('${SRCROOT}', relative, 'App.framework', 'App'),
#      File.join('${SRCROOT}', relative, 'engine', 'Flutter.framework', 'Flutter'),
#      flutter_export_environment_path
#    ],
#    :execution_position => :before_compile
end

#def parse_KV_file(file, separator='=')
#  file_abs_path = File.expand_path(file)
#  if !File.exists? file_abs_path
#    return [];
#  end
#  pods_array = []
#  skip_line_start_symbols = ["#", "/"]
#  File.foreach(file_abs_path) { |line|
#    next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
#    plugin = line.split(pattern=separator)
#    if plugin.length == 2
#      podname = plugin[0].strip()
#      path = plugin[1].strip()
#      podpath = File.expand_path("#{path}", file_abs_path)
#      pods_array.push({:name => podname, :path => podpath});
#     else
#      puts "Invalid plugin specification: #{line}"
#    end
#  }
#  return pods_array
#end

#def flutter_root
#  generated_xcode_build_settings = parse_KV_file(File.join(__dir__, 'Generated.xcconfig'))
#  if generated_xcode_build_settings.empty?
#    puts "Generated.xcconfig must exist. Make sure `flutter pub get` is executed in the Flutter module."
#    exit
#  end
#  generated_xcode_build_settings.map { |p|
#    if p[:name] == 'FLUTTER_ROOT'
#      return p[:path]
#    end
#  }
#end

解释一下,这个脚本未注释的部分做了两件事:

  • 引入Flutter引擎install_flutter_engine_pod
  • 引入Flutter工程编译后的Framework,install_flutter_application_pod

如果是执行flutter build ios --debug会把这个脚本里的已注释代码部分取消注释。原因是release模式会把第三方库的东西都统一编译到App.framework内,debug不会,所以在debug时会把第三方库分别引入。

方案

综上,我们的方案步骤如下:

  1. 在flutter_module工程里执行flutter build ios --release方法,在工程目录里找到.ios文件夹

  2. 复制出来Flutter文件夹下的engine目录,App.framework及flutter module的podspec文件。

  3. 将app.framework及podspec放入一个文件夹,engine放入另一个文件夹。

  4. podfile里面添加

    pod 'Flutter', :path => 'path to engine'
    pod 'flutter module 的name', :path => 'path to app.framework'
    

执行pod install就好了

此外,也可以把这两个pod库放入git仓库等进行管理。

参考文章:

Flutter远程依赖简单实践
Flutter iOS 混合工程自动化

欢迎大家拍砖

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容