0%

Flutter 系列之 iOS APP 构建流程(一)

本文为 Flutter 学习系列文章的第一篇,主要介绍 iOS APP 的启动流程的前绪部分,包括 iOS APP 工程的目录结构,Flutter Framework 的构建,以及 FlutterViewController 的创建等。

Flutter iOS Framework 源码地址: https://github.com/flutter/engine/tree/master/shell/platform/darwin/ios/framework/Source

工程目录

安装并配置好 Flutter 之后,我们在命名行中执行 flutter create myapp 就能够创建一个 flutter 项目,包括 iOS 和 android 两个平台的代码。 代码目录结构如下:

Flutter 的安装及配置参考 https://flutterchina.club/get-started/install/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
╰─○ ls -al myapp
total 56
drwxr-xr-x 14 chujiayi staff 448 12 19 18:46 .
drwxr-xr-x 8 chujiayi staff 256 12 19 18:45 ..
-rw-r--r-- 1 chujiayi staff 1491 12 19 18:45 .gitignore
drwxr-xr-x 6 chujiayi staff 192 12 19 18:45 .idea
-rw-r--r-- 1 chujiayi staff 305 12 19 18:45 .metadata
-rw-r--r-- 1 chujiayi staff 2477 12 19 18:46 .packages
-rw-r--r-- 1 chujiayi staff 535 12 19 18:45 README.md
drwxr-xr-x 11 chujiayi staff 352 12 19 18:46 android
drwxr-xr-x 6 chujiayi staff 192 12 19 18:45 ios
drwxr-xr-x 3 chujiayi staff 96 12 19 18:45 lib
-rw-r--r-- 1 chujiayi staff 896 12 19 18:45 myapp.iml
-rw-r--r-- 1 chujiayi staff 3279 12 19 18:46 pubspec.lock
-rw-r--r-- 1 chujiayi staff 2657 12 19 18:45 pubspec.yaml
drwxr-xr-x 3 chujiayi staff 96 12 19 18:45 test

其中主要有以下几个目录:

  • ios :存放 iOS 平台的代码的目录
  • android :存放 android 平台的代码的目录
  • lib,存放 dart 代码的目录。

继续看 ios 目录。该目录包含了运行 iOS APP 所需的代码,由 Flutter 自动生成。我们可以直接在 Xcode 中打开 Runner.xcworkspace,就能运行 APP 了。

也可以直接在 myapp 目录执行 flutter run ios

1
2
3
4
5
6
7
8
╰─○ ls -al myapp/ios 
total 0
drwxr-xr-x 6 chujiayi staff 192 12 19 18:45 .
drwxr-xr-x 14 chujiayi staff 448 12 19 18:46 ..
drwxr-xr-x 7 chujiayi staff 224 12 19 18:46 Flutter
drwxr-xr-x 9 chujiayi staff 288 12 19 18:46 Runner
drwxr-xr-x 5 chujiayi staff 160 12 19 18:45 Runner.xcodeproj
drwxr-xr-x 3 chujiayi staff 96 12 19 18:45 Runner.xcworkspace

Flutter动态库生成

打开 Xcode 之后,我们会发现在Xcode工程目录的 Flutter 子目录下有两个红色的文件, App.frameworkFlutter.framework ,说明现在这两个 framework 并不存在。从命令行输出结果来看,也确实没有这两个文件。

1
2
3
4
5
6
7
8
9
╰─○ ls -al myapp/ios/Flutter
total 40
drwxr-xr-x 7 chujiayi staff 224 12 19 19:01 .
drwxr-xr-x 6 chujiayi staff 192 12 19 19:01 ..
-rw-r--r--@ 1 chujiayi staff 794 9 10 2019 AppFrameworkInfo.plist
-rw-r--r--@ 1 chujiayi staff 30 9 10 2019 Debug.xcconfig
-rw-r--r-- 1 chujiayi staff 433 12 19 19:01 Generated.xcconfig
-rw-r--r--@ 1 chujiayi staff 30 9 10 2019 Release.xcconfig
-rwxr-xr-x 1 chujiayi staff 514 12 19 19:01 flutter_export_environment.sh

那么这两个 framework 是什么时候生成的呢?里面又包含了哪些东西呢?
先不用想这么多,我们不妨先尝试在 Xcode 中运行下项目,看看会不会有报错。

一运行,不仅没有任何报错,而且这两个 framework 竟然自动出现了!所以是我们点击运行时,XCode 自动生成了这来给你个 framework ?
我们再次查看Flutter目录,发现确实多了 App.frameworkFlutter.framework

1
2
3
4
5
6
7
8
9
10
11
╰─○ ls -al myapp/ios/Flutter
total 40
drwxr-xr-x 9 chujiayi staff 288 12 19 20:18 .
drwxr-xr-x 7 chujiayi staff 224 12 19 20:25 ..
drwxr-xr-x 5 chujiayi staff 160 12 19 20:18 App.framework
-rw-r--r--@ 1 chujiayi staff 794 9 10 2019 AppFrameworkInfo.plist
-rw-r--r--@ 1 chujiayi staff 30 9 10 2019 Debug.xcconfig
drwxr-xr-x@ 8 chujiayi staff 256 12 19 20:18 Flutter.framework
-rw-r--r-- 1 chujiayi staff 433 12 19 19:01 Generated.xcconfig
-rw-r--r--@ 1 chujiayi staff 30 9 10 2019 Release.xcconfig
-rwxr-xr-x 1 chujiayi staff 514 12 19 19:01 flutter_export_environment.sh

其实很容易想到,一定是最开始执行 flutter create myapp 时,我们的 iOS 工程被做了手脚。
哪里最有可能被做手脚呢?一定是 Build Phase 中添加了自定义的 Run Script

果不其然,我们在 Xcode 中找到了“罪魁祸首” – 一个特殊的 Run Script

1
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

这个脚本存在于 Flutter 的安装目录下。代码有点多,我们挑重点部分来看:

脚本完整代码可查看 https://github.com/flutter/flutter/blob/master/packages/flutter_tools/bin/xcode_backend.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Adds the App.framework as an embedded binary and the flutter_assets as
# resources.
EmbedFlutterFrameworks() {
# Embed App.framework from Flutter into the app (after creating the Frameworks directory
# if it doesn't already exist).
local xcode_frameworks_dir="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
RunCommand mkdir -p -- "${xcode_frameworks_dir}"
RunCommand rsync -av --delete --filter "- .DS_Store/" "${BUILT_PRODUCTS_DIR}/App.framework" "${xcode_frameworks_dir}"

# 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.

# Copy Xcode behavior and don't copy over headers or modules.
RunCommand rsync -av --delete --filter "- .DS_Store/" --filter "- Headers/" --filter "- Modules/" "${BUILT_PRODUCTS_DIR}/Flutter.framework" "${xcode_frameworks_dir}/"
if [[ "$ACTION" != "install" || "$ENABLE_BITCODE" == "NO" ]]; then
# Strip bitcode from the destination unless archiving, or if bitcode is disabled entirely.
RunCommand "${DT_TOOLCHAIN_DIR}"/usr/bin/bitcode_strip "${BUILT_PRODUCTS_DIR}/Flutter.framework/Flutter" -r -o "${xcode_frameworks_dir}/Flutter.framework/Flutter"
fi

# Sign the binaries we moved.
if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ]]; then
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/App.framework/App"
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter"
fi

AddObservatoryBonjourService
}

上面这部分的代码注释已经很清晰了,主要就是生成 App.frameworkFlutter.framework 并放置到对应目录下。 生成工程项目的时候,Xcode 中已经引用了这两个 framework ,只不过当时它们还不存在;一执行编译,这两个 framework 就会自动生成,也就不会有任何报错了。

其中,Flutter.frameworkFlutter Engine 部分的代码,由 C++ 编译而成。 App.framework (在Release模式下)包含了 dart AOT 编译后的产物)。

这两个库都是动态库,我们可以直接通过 file 命令进行验证。

1
2
╰─○ file myapp/ios/Flutter/App.framework/App 
myapp/ios/Flutter/App.framework/App: Mach-O 64-bit dynamically linked shared library x86_64

总结:Flutter 项目会额外引入两个动态库,包含 Flutter Engine 和 Dart 代码 等。

Flutter动态库加载

在执行 flutter create myapp 创建项目时,Flutter 就会在 Xcode 工程文件中添加这两个 framework 的引用。在 ** Xcode Build Phase** 的 Link Binary with LibrariesEmbed Frameworks 中,我们都能看到这两个库的存在。

1
2
3
4
5
6
7
8
9
10
11
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

因此,在 APP 的启动阶段,这两个动态库就会被自动加载。

APP 生命周期处理

继续观察之前创建好的项目,其实基本没有什么代码,主要就有一个 AppDelegate 类,继承自 FlutterAppDelegate

1
2
3
4
5
6
7
8
9
10
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

FlutterAppDelegate 类在 Flutter.framework 中定义。 FlutterAppDelegate 又继续将生命周期回调派发给各个 FlutterPluginAppLifeCycleDelegate,以便各个 Plugin 处理 APP 生命周期回调事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
for (NSObject<FlutterApplicationLifeCycleDelegate>* delegate in [_delegates allObjects]) {
if (!delegate) {
continue;
}
if ([delegate respondsToSelector:_cmd]) {
if (![delegate application:application didFinishLaunchingWithOptions:launchOptions]) {
return NO;
}
}
}
return YES;
}

FlutterViewController 和 FlutterView

FlutterAppDelegate 类似乎并没有做什么其他事情,那 Flutter 的页面是怎么展示到屏幕上的呢?
如果我们去看 APP 的页面层级的话,会发现最顶层为一个 FlutterViewController ,其 viewFlutterView

Flutter 创建的 APP 默认使用了 storyboard ,并在 storyboard 中将 rootViewController 设置为 FlutterViewControllerFlutterViewControllerFlutterView 就承载了具体的 Flutter 页面。

FlutterViewController 有两个新增的 designated initializer,其中默认调用的是 initWithProject:nibName:bundle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@interface FlutterViewController : UIViewController <FlutterTextureRegistry, FlutterPluginRegistry>

/**
* Initializes this FlutterViewController with the specified `FlutterEngine`.
*
* The initialized viewcontroller will attach itself to the engine as part of this process.
*
* @param engine The `FlutterEngine` instance to attach to.
* @param nibNameOrNil The NIB name to initialize this UIViewController with.
* @param nibBundleOrNil The NIB bundle.
*/
- (instancetype)initWithEngine:(FlutterEngine*)engine
nibName:(NSString*)nibNameOrNil
bundle:(NSBundle*)nibBundleOrNil NS_DESIGNATED_INITIALIZER;

/**
* Initializes a new FlutterViewController and `FlutterEngine` with the specified
* `FlutterDartProject`.
*
* @param projectOrNil The `FlutterDartProject` to initialize the `FlutterEngine` with.
* @param nibNameOrNil The NIB name to initialize this UIViewController with.
* @param nibBundleOrNil The NIB bundle.
*/
- (instancetype)initWithProject:(FlutterDartProject*)projectOrNil
nibName:(NSString*)nibNameOrNil
bundle:(NSBundle*)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
1
2
3
- (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
return [self initWithProject:nil nibName:nil bundle:nil];
}

我们注意到,另一个初始化方法 initWithEngine:nibName:bundle: 支持传入一个 FlutterEngine 参数,这样就能够实现 FlutterEngine 的复用。但是要注意,同一时刻一个 FlutterEngine 最多只能绑定到一个 FlutterViewController 上,否则调用该初始化方法时就会直接报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (instancetype)initWithEngine:(FlutterEngine*)engine
nibName:(nullable NSString*)nibName
bundle:(nullable NSBundle*)nibBundle {
NSAssert(engine != nil, @"Engine is required");
self = [super initWithNibName:nibName bundle:nibBundle];
if (self) {
_viewOpaque = YES;
if (engine.viewController) {
FML_LOG(ERROR) << "The supplied FlutterEngine " << [[engine description] UTF8String]
<< " is already used with FlutterViewController instance "
<< [[engine.viewController description] UTF8String]
<< ". One instance of the FlutterEngine can only be attached to one "
"FlutterViewController at a time. Set FlutterEngine.viewController "
"to nil before attaching it to another FlutterViewController.";
}
// 此处省略 ...
}
return self;
}

总结

文本主要介绍了 Flutter APP 工程目录、iOS Framework 的生成以及 FlutterViewController 的基本代码等,大家应该对 Flutter iOS APP 的构建流程有了基本的了解。

当然我们也遗留了很多问题还没解释清楚,比如 Flutter 页面最终是如何渲染到屏幕上的? Flutter Engine 的作用是什么? iOS 与 android 的启动流程有什么区别? 这些问题都留待后续文章继续探讨。

参考