之前写了一篇关于如何将 flutter 直接打包成 android aar 的文章, 本篇写一写如何将 flutter 打包成 framework 以便于直接让没有 flutter 环境的 iOS 开发者使用, 因为国内很多项目都有这样的要求

本篇并不会做完全的工程集成化, 只是做一下如何将 flutter 的 framework 打出来, 并且置入到 iOS 原生工程中, 因为各项目一定户会有自己的特殊性, 不可能完全一样

本篇打包脚本部分参考了 https://www.jianshu.com/p/700bd7d2122b 的内容,但是又有一些针对 flutter 版本的变化和 flutter type 不同的情况进行的修改, 不观看连接中的内容并不会影响观看

开发环境

MacOS
XCode 10
git
flutter 及 flutter 的相关工具链
cocoapods

创建几个工程

iOS 原生工程

使用 xcode 创建 img

img

这个原生工程就是模拟你的原有工程

Flutter 工程

这里我只使用 flutter module 的方式, 如果你 flutter 是 app 的方式创建的,则打包脚本的内容需要根据应用结构有所调整

$ flutter create -t module flutter_module_for_ios

这次直接在里面添加一个带有原生功能的插件, 和 android 篇相同依然选择 shared_preferences 那个插件

修改 pubspec.yaml:

dependencies:
  shared_preferences: ^0.5.3+1

创建脚本

观察结构

打包 iOS,然后观察 build 文件夹

flutter packages get
flutter build ios --release --no-codesign
tree build/ios/Release-iphoneos
build/ios/Release-iphoneos
├── FlutterPluginRegistrant
│   └── libFlutterPluginRegistrant.a
├── Runner.app
│   ├── AppIcon20x20@2x.png
│   ├── AppIcon20x20@2x~ipad.png
│   ├── AppIcon20x20@3x.png
│   ├── AppIcon20x20~ipad.png
│   ├── AppIcon29x29.png
│   ├── AppIcon29x29@2x.png
│   ├── AppIcon29x29@2x~ipad.png
│   ├── AppIcon29x29@3x.png
│   ├── AppIcon29x29~ipad.png
│   ├── AppIcon40x40@2x.png
│   ├── AppIcon40x40@2x~ipad.png
│   ├── AppIcon40x40@3x.png
│   ├── AppIcon40x40~ipad.png
│   ├── AppIcon60x60@2x.png
│   ├── AppIcon60x60@3x.png
│   ├── AppIcon76x76@2x~ipad.png
│   ├── AppIcon76x76~ipad.png
│   ├── AppIcon83.5x83.5@2x~ipad.png
│   ├── Assets.car
│   ├── Base.lproj
│   │   ├── LaunchScreen.storyboardc
│   │   │   ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib
│   │   │   ├── Info.plist
│   │   │   └── UIViewController-01J-lp-oVM.nib
│   │   └── Main.storyboardc
│   │       ├── BYZ-38-t0r-view-8bC-Xf-vdC.nib
│   │       ├── Info.plist
│   │       └── UIViewController-BYZ-38-t0r.nib
│   ├── Debug.xcconfig
│   ├── Flutter.xcconfig
│   ├── Frameworks
│   │   ├── App.framework
│   │   │   ├── App
│   │   │   ├── Info.plist
│   │   │   └── flutter_assets
│   │   │       ├── AssetManifest.json
│   │   │       ├── FontManifest.json
│   │   │       ├── LICENSE
│   │   │       ├── fonts
│   │   │       │   └── MaterialIcons-Regular.ttf
│   │   │       └── packages
│   │   │           └── cupertino_icons
│   │   │               └── assets
│   │   │                   └── CupertinoIcons.ttf
│   │   └── Flutter.framework
│   │       ├── Flutter
│   │       ├── Info.plist
│   │       └── icudtl.dat
│   ├── Info.plist
│   ├── PkgInfo
│   ├── Release.xcconfig
│   └── Runner
├── Runner.app.dSYM
│   └── Contents
│       ├── Info.plist
│       └── Resources
│           └── DWARF
│               └── Runner
├── libPods-Runner.a
└── shared_preferences
    └── libshared_preferences.a

18 directories, 46 files

发现打包出来的是.a 文件, 这里我们需要修改一下 ios 目录下的文件, 以便于打包出来 framework 文件, 因为 framework 是 apple 提供的一种打包方案, 直接将所有需要的资源包括头文件,库文件都聚集到了一起,方便引用, 而.a 文件就不一样了, 还需要包含对应的头文件

修改.ios 下的 podfile

在第一行添加这个

use_frameworks!

然后先清除一下刚刚打包的内容 $ flutter clean

flutter build ios --release --no-codesign
tree build/ios/Release-iphoneos
build/ios/Release-iphoneos
├── FlutterPluginRegistrant
│   ├── FlutterPluginRegistrant.framework
│   │   ├── FlutterPluginRegistrant
│   │   ├── Headers
│   │   │   ├── FlutterPluginRegistrant-umbrella.h
│   │   │   └── GeneratedPluginRegistrant.h
│   │   ├── Info.plist
│   │   └── Modules
│   │       └── module.modulemap
│   └── FlutterPluginRegistrant.framework.dSYM
│       └── Contents
│           ├── Info.plist
│           └── Resources
│               └── DWARF
│                   └── FlutterPluginRegistrant
├── Pods_Runner.framework
│   ├── Headers
│   │   └── Pods-Runner-umbrella.h
│   ├── Info.plist
│   ├── Modules
│   │   └── module.modulemap
│   └── Pods_Runner
├── Runner.app
│   ├── AppIcon20x20@2x.png
│   ├── AppIcon20x20@2x~ipad.png
│   ├── AppIcon20x20@3x.png
│   ├── AppIcon20x20~ipad.png
│   ├── AppIcon29x29.png
│   ├── AppIcon29x29@2x.png
│   ├── AppIcon29x29@2x~ipad.png
│   ├── AppIcon29x29@3x.png
│   ├── AppIcon29x29~ipad.png
│   ├── AppIcon40x40@2x.png
│   ├── AppIcon40x40@2x~ipad.png
│   ├── AppIcon40x40@3x.png
│   ├── AppIcon40x40~ipad.png
│   ├── AppIcon60x60@2x.png
│   ├── AppIcon60x60@3x.png
│   ├── AppIcon76x76@2x~ipad.png
│   ├── AppIcon76x76~ipad.png
│   ├── AppIcon83.5x83.5@2x~ipad.png
│   ├── Assets.car
│   ├── Base.lproj
│   │   ├── LaunchScreen.storyboardc
│   │   │   ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib
│   │   │   ├── Info.plist
│   │   │   └── UIViewController-01J-lp-oVM.nib
│   │   └── Main.storyboardc
│   │       ├── BYZ-38-t0r-view-8bC-Xf-vdC.nib
│   │       ├── Info.plist
│   │       └── UIViewController-BYZ-38-t0r.nib
│   ├── Debug.xcconfig
│   ├── Flutter.xcconfig
│   ├── Frameworks
│   │   ├── App.framework
│   │   │   ├── App
│   │   │   ├── Info.plist
│   │   │   └── flutter_assets
│   │   │       ├── AssetManifest.json
│   │   │       ├── FontManifest.json
│   │   │       ├── LICENSE
│   │   │       ├── fonts
│   │   │       │   └── MaterialIcons-Regular.ttf
│   │   │       └── packages
│   │   │           └── cupertino_icons
│   │   │               └── assets
│   │   │                   └── CupertinoIcons.ttf
│   │   ├── Flutter.framework
│   │   │   ├── Flutter
│   │   │   ├── Info.plist
│   │   │   └── icudtl.dat
│   │   ├── FlutterPluginRegistrant.framework
│   │   │   ├── FlutterPluginRegistrant
│   │   │   └── Info.plist
│   │   └── shared_preferences.framework
│   │       ├── Info.plist
│   │       └── shared_preferences
│   ├── Info.plist
│   ├── PkgInfo
│   ├── Release.xcconfig
│   └── Runner
├── Runner.app.dSYM
│   └── Contents
│       ├── Info.plist
│       └── Resources
│           └── DWARF
│               └── Runner
└── shared_preferences
    ├── shared_preferences.framework
    │   ├── Headers
    │   │   ├── SharedPreferencesPlugin.h
    │   │   └── shared_preferences-umbrella.h
    │   ├── Info.plist
    │   ├── Modules
    │   │   └── module.modulemap
    │   └── shared_preferences
    └── shared_preferences.framework.dSYM
        └── Contents
            ├── Info.plist
            └── Resources
                └── DWARF
                    └── shared_preferences

37 directories, 65 files

Runner.app 这个熟悉的名字下虽然看似包含所有的 framework,但是仔细观察, 这些 framework 内不包含头文件, 所以其实是用不了的

可以使用的是如图所示的几个

img

根据上面的 tree 图可以看到,其中包含了全部的库文件, 但除此以外,还需要两个库:

  1. App.framework: 这个包含了 flutter 的资源
  2. Flutter.framework: 这个是 Flutter 的 engine 运行库, 每个 flutter 应用必然有的东西

这两个东西需要从.ios 中找到

  1. App.framework: .ios/Flutter/App.framework
  2. Flutter.framework: .ios/Flutter/engine/Flutter.framework

这样就集齐了所有的库文件, 问题是, 这样是手动做的, 我们需要一个自动化的方案把重复的工作都完成

所以我根据开头的文章内的脚本编写了一个脚本:

if [ -z $out ]; then
    out='ios_frameworks'
fi

echo "准备输出所有文件到目录: $out"

find . -d -name build | xargs rm -rf
flutter clean
rm -rf $out
rm -rf build

flutter packages get
flutter build ios --release --no-codesign

mkdir $out

cp -r build/ios/Release-iphoneos/*/*.framework $out
cp -r .ios/Flutter/App.framework $out
cp -r .ios/Flutter/engine/Flutter.framework $out

接着可以通过sh build_ios.sh 来自动打包,并且将所有文件复制到 ios_frameword 目录下, 嗯 这个目录可以通过 out=ios_out sh build_ios.sh来改变

关联库文件和工程

原生工程的管理方式有很多种, 比如只使用 xcode 来管理

cocoapod(支持 oc 和 swift)

单独针对 swift 还有 Carthage, Swift Package Manager

当然这里我还是用 cocoapod 的方案, 其他的方案请自行研究

使用 cocoapod 管理原生工程

为原生工程添加一个 Podfile

cd top.kikt.existsapp
touch Podfile
code Podfile # 这一步是用 vscode 打开 你也可以用 vim 或其他任何文本编辑器

修改 Podfile

platform :ios, '8.0'
use_frameworks!

target 'top.kikt.existsapp' do

end

然后继续在命令行里 $ pod install

如果没有报错, 则说明原生项目现在已经被 pod 管理了

将 framework 作为一个 pod 库

因为找遍了 podfile 的相关文档, 没有找到可以直接引用 framework 的方式

所以需要一个 pod 库作为”中转”

新建一个库 $ pod lib create flutter-lib, 然后按顺序回答问题, 这目录根据你自己的实际情况来

我的是和 iOS 原生同级目录

$ pod lib create flutter-lib
Cloning `https://github.com/CocoaPods/pod-template.git` into `flutter-lib`.
Configuring flutter-lib template.

------------------------------

To get you started we need to ask a few questions, this should only take a minute.

If this is your first time we recommend running through with the guide:
 - https://guides.cocoapods.org/making/using-pod-lib-create.html
 ( hold cmd and double click links to open in a browser. )


What platform do you want to use?? [ iOS / macOS ]
 >
ios
What language do you want to use?? [ Swift / ObjC ]
 > objc

Would you like to include a demo application with your library? [ Yes / No ]
 > no

Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > none

Would you like to do view based testing? [ Yes / No ]
 > no

What is your class prefix?
 >

You need to provide an answer.
What is your class prefix?
 > FFF

Running pod install on your new library.

Analyzing dependencies
Fetching podspec for `flutter-lib` from `../`
Downloading dependencies
Installing flutter-lib (0.1.0)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `flutter-lib.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

[!] Automatically assigning platform `ios` with version `9.3` on target `flutter-lib_Tests` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.

 Ace! you're ready to go!
 We will start you off by opening your project in Xcode
  open 'flutter-lib/Example/flutter-lib.xcworkspace'

To learn more about the template see `https://github.com/CocoaPods/pod-template.git`.
To learn more about creating a new pod, see `https://guides.cocoapods.org/making/making-a-cocoapod`.

创建完成后, 修改 flutter-lib.podspec 文件

这里需要先把刚刚的 framework 复制到项目中

默认有一堆注释都可以删掉

然后添加这么一行

s.ios.vendored_frameworks = 'ios_frameworks/App.framework', 'ios_frameworks/Flutter.framework', 'ios_frameworks/FlutterPluginRegistrant.framework', 'ios_frameworks/shared_preferences.framework'

这个方案是临时的, 后面肯定会写成动态查找的方案, 不可能有一个库写一次

现在我的 podspec 文件是这样的

Pod::Spec.new do |s|
  s.name             = 'flutter-lib'
  s.version          = '0.1.0'
  s.summary          = 'A short description of flutter-lib.'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  s.homepage         = 'https://github.com/cjl_spy@163.com/flutter-lib'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'cjl_spy@163.com' => 'cjl_spy@163.com' }
  s.source           = { :git => 'https://github.com/cjl_spy@163.com/flutter-lib.git', :tag => s.version.to_s }
  s.ios.deployment_target = '8.0'
  s.static_framework = true
  # s.source_files = 'flutter-lib/Classes/**/*'
  s.ios.vendored_frameworks = 'ios_frameworks/App.framework', 'ios_frameworks/Flutter.framework', 'ios_frameworks/FlutterPluginRegistrant.framework', 'ios_frameworks/shared_preferences.framework'
end

在原生项目中引用这个库

platform :ios, '8.0'
use_frameworks!

target 'top.kikt.existsapp' do
   pod 'flutter-lib', :path => '../flutter-lib'
end

接着在原生项目中$ pod install

运行项目可能会有两种错误:

信息包含什么 System 之类的, 这个是需要修改构建系统

img

运行项目还可能会报一个 bitcode 的错误, 这里需要修改一个选项 img 将这里修改为 NO

我的项目目前为止能跑起来不报错了, 但是目前项目还没有启动 Flutter 的页面, 到这一步后我是按照 flutter 官方 wiki 进行修改的

我给的例子仅有 oc 的,swift 的请自行参考 wiki

修改AppDelegate.h

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

AppDelegate.m

#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins

#include "AppDelegate.h"

@implementation AppDelegate

// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
  [self.flutterEngine runWithEntrypoint:nil];
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

接着就是启动 Flutter 的事情了, 这个根据你业务逻辑自行实现, 核心代码是如下几行

#import <Flutter/Flutter.h>
#import "AppDelegate.h"
#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(handleButtonAction)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Press me" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor blueColor]];
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)handleButtonAction {
    FlutterEngine *flutterEngine = [(AppDelegate *)[[UIApplication sharedApplication] delegate] flutterEngine];
    FlutterViewController *flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
    [self presentViewController:flutterViewController animated:false completion:nil];
}
@end

优化脚本

优化 podspec 文件

这一步的目的是, 遍历文件夹, 不再单独的每个引用 framework 文件

podspec 使用的是 ruby 语法, 也就是说本身就支持编程, 这时候如果再用 shell 或者 dart 就有点增加复杂度了

如果不了解 ruby 语法, 你也可以用自己的方法解决(shell,python,dart 什么都随你), 我也是边写边看的 ruby 语法, 按照如下方式修改

Pod::Spec.new do |s|
  s.name             = 'flutter-lib'
  s.version          = '0.1.0'
  s.summary          = 'A short description of flutter-lib.'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  s.homepage         = 'https://github.com/cjl_spy@163.com/flutter-lib'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'cjl_spy@163.com' => 'cjl_spy@163.com' }
  s.source           = { :git => 'https://github.com/cjl_spy@163.com/flutter-lib.git', :tag => s.version.to_s }
  s.ios.deployment_target = '8.0'
  s.static_framework = true
  # s.source_files = 'flutter-lib/Classes/**/*'
  p = Dir::open("ios_frameworks")
  arr = Array.new
  p.each do |f|
    if f == '.' || f == '..'
    else
        arr.push('ios_frameworks/'+f)
    end
  end

  s.ios.vendored_frameworks = arr
end

然后执行 pod install

优化 shell 脚本

if [ -z $out ]; then
    out='ios_frameworks'
fi

echo "准备输出所有文件到目录: $out"

find . -d -name build | xargs rm -rf
flutter clean
rm -rf $out
rm -rf build

flutter packages get
flutter build ios --release --no-codesign

mkdir $out

cp -r build/ios/Release-iphoneos/*/*.framework $out
cp -r .ios/Flutter/App.framework $out
cp -r .ios/Flutter/engine/Flutter.framework $out

cp -r $out ../flutter-lib/ios_frameworks # 添加这步

验证下别的插件

添加插件到 pubspec.yaml

dependencies:
  path_provider: ^1.1.0

$ flutter pub get

这里有个小坑,当添加了 pubspec 插件后, flutter/.ios/Podfile 的内容会被恢复成默认状态

所以需要再修改一下 shell 脚本:

if [ -z $out ]; then
    out='ios_frameworks'
fi

echo "准备输出所有文件到目录: $out"

echo "清除所有已编译文件"
find . -d -name build | xargs rm -rf
flutter clean
rm -rf $out
rm -rf build

flutter packages get

addFlag(){
    cat .ios/Podfile > tmp1.txt
    echo "use_frameworks!" >> tmp2.txt
    cat tmp1.txt >> tmp2.txt
    cat tmp2.txt > .ios/Podfile
    rm tmp1.txt tmp2.txt
}

echo "检查 .ios/Podfile文件状态"
a=$(cat .ios/Podfile)
if [[ $a == use* ]]; then
    echo '已经添加use_frameworks, 不再添加'
else
    echo '未添加use_frameworks,准备添加'
    addFlag
    echo "添加use_frameworks 完成"
fi

echo "编译flutter"
flutter build ios --release --no-codesign

echo "编译flutter完成"
mkdir $out

cp -r build/ios/Release-iphoneos/*/*.framework $out
cp -r .ios/Flutter/App.framework $out
cp -r .ios/Flutter/engine/Flutter.framework $out

echo "复制framework库到临时文件夹: $out"

libpath='../flutter-lib'

rm -rf "$libpath/ios_frameworks"
mkdir $libpath
cp -r $out $libpath

echo "复制库文件到: $libpath"

顺便润色了一下,加了一点日志

运行下项目, 跑了一下, 果然没有问题了

这里有一个插曲请注意,我脚本里用了 gsed 命令,这个工具是 GNU-sed 的缩写, 因为 mac 的 sed 命令有一些问题比较难用
请使用$ brew install gnu-sed安装一下, 否则这个命令可能失败

备注: 因为有反馈, gsed 在不同的 mac 上会出现把文本变成一行的问题, 现在采用另一种方案,不再需要 gsed

总结

建议: 请将 flutter,原生和 flutter 的 framework 所在的 pod 库作为 3 个单独的库,便于管理(有 pod 的私服请按照自己的方案实现 framework 的共享)

然后脚本中的目录什么的需要根据你的实际情况进行调整

打包的使用总结

flutter 方:

  1. 创建一个 pod 库
  2. 使用脚本打包并把所有的文件复制到 pod 库内
  3. git commit + push

ios 方:

  1. git pull
  2. pod install
  3. run

后记

到目前为止, iOS 也算是完成了加入到已有工程, 原生方只使用 pod+git 理论上就达到了可以不用 flutter 依然可运行的目的

仓库看这里: https://github.com/CaiJingLong/add_flutter_to_exists_ios_example

以上