在 Xcode 中集成 frameworks
因为官方推荐的第一种方案未测试通过,且根据我们的情况,第二种方案更加贴合一些,所以我没有在第一种方案上继续纠结研究,转而使用第二种方案。
第二种方案不需要CocoaPods,首先编译打包flutter module:
flutter build ios-framework --xcframework --no-universal --output=./Flutter/
会在flutter module目录下生成一个Flutter目录,里面产出编译后的framework,如下:
flutter module/ └── Flutter/ ├── Debug/ │ ├── Flutter.xcframework │ ├── App.xcframework │ ├── FlutterPluginRegistrant.xcframework (only if you have plugins with iOS platform code) │ └── example_plugin.xcframework (each plugin is a separate framework) ├── Profile/ │ ├── Flutter.xcframework │ ├── App.xcframework │ ├── FlutterPluginRegistrant.xcframework (only if you have plugins with iOS platform code) │ └── example_plugin.xcframework (each plugin is a separate framework) └── Release/ ├── Flutter.xcframework ├── App.xcframework ├── FlutterPluginRegistrant.xcframework (only if you have plugins with iOS platform code) └── example_plugin.xcframework (each plugin is a separate framework) 复制代码
我们可以将这个Flutter目录拷贝到ios项目下,然后在ios项目的Build Phases下的Link Binary With Libraries下添加framework,直接将Flutter.xcframework和App.xcframework等文件(注意:这里官方上使用的是release目录下的,但是我先使用的是Debug目录下的文件,后续会解释这里,先记录一下)拖拽进去即可,如下:
注意:这一步官网上还在Build Settings -> Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 中增加 $(PROJECT_DIR)/Flutter/Release/。但是这个应该是与上面添加framework文件效果是一样的。我只做了上面添加文件,没有设置这个运行是没有问题的。不知道如果同时设置会不会出现什么问题。
然后需要将framework内嵌(embed)到项目,在项目的General下的FrameWorks, Libraries, and Embedded Content下,将刚才加入的framework改成Embed & Sign,如下:
然后⌘+B 编译项目即可。 这个过程还算顺利,没有出现什么问题。
ios中启动flutter页面
参考官方教程:flutter.cn/docs/develo…
先是修改AppDelegate文件,修改成:
import UIKit import Flutter @UIApplicationMain class AppDelegate: FlutterAppDelegate { lazy var flutterEngine = FlutterEngine(name: "flutter engine") override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. flutterEngine.run() return true } // MARK: UISceneSession Lifecycle override func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } override func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } } 复制代码
然后修改ViewController文件:
import UIKit import Flutter class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let button = UIButton(type:UIButton.ButtonType.custom) button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside) button.setTitle("Show Flutter!", for: UIControl.State.normal) button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0) button.backgroundColor = UIColor.blue self.view.addSubview(button) } @objc func showFlutter() { let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil) present(flutterViewController, animated: true, completion: nil) } } 复制代码
然后运行即可。
就这样?显然不可能,下面说说我遇到的几个问题:
编译失败 building for iOS Simulator-arm64 but attempting to link with file built for iOS Simulator-x86_64
报错如下:
ld: warning: ignoring file xxx/Build/Products/Debug-iphonesimulator/App.framework/App, building for iOS Simulator-arm64 but attempting to link with file built for iOS Simulator-x86_64
ld: warning: ignoring file xxx/Build/Products/Debug-iphonesimulator/Flutter.framework/Flutter, building for iOS Simulator-arm64 but attempting to link with file built for iOS Simulator-x86_64
Undefined symbols for architecture arm64:
"OBJC_CLASS$_FlutterAppDelegate", referenced from:
type metadata for iostest2.AppDelegate in AppDelegate.o 复制代码
"OBJC_CLASS$_FlutterEngine", referenced from:
objc-class-ref in AppDelegate.o 复制代码
"OBJC_METACLASS$_FlutterAppDelegate", referenced from:
_OBJC_METACLASS_$__TtC8iostest211AppDelegate in AppDelegate.o 复制代码
"OBJC_CLASS$_FlutterViewController", referenced from:
objc-class-ref in ViewController.o 复制代码
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
很明显是cpu架构的问题,但是为什么会出现这样的问题?我们看之前生成的flutter framework文件,拿Debug目录下的App.xcframework为例,这个目录下的文件如下:
可以看到在simulator(模拟器)上是x86_64的,而在真机上则是arm64_armv7的。从上面报错日志上看,程序是想找arm64下的文件,但是我们是打算运行到模拟器上的,所以找不到了文件。
这个问题官网上flutter.cn/docs/develo… 的最后也提到了,解决方法是在项目的Build Settings -> Archiectures -> Excluded Archiectures下将simulator都设置arm64即可,如下:
鼠标移到Debug上,后面会出现+号,点击就会在下面添加一条。
然后在新添加的左侧选择Any iOS Simulator SDK,双击右侧就会弹窗,在弹窗中添加一条arm64即可。
同样在Release下也操作一下,最后完成效果如上图。
这样设置后在模拟器上编译运行时就会排除arm64。再进行编译即可通过。
运行后提示Engine run configuration was invalid. Could not launch engine with configuration.
运行后,在日志区域显示如下日志:
Engine run configuration was invalid.
Could not launch engine with configuration.
点击按钮无法正常显示flutter页面。
根据网上一个大神的解释,这是因为物料出问题了(如果你上面按照我的提示做的就不会出现这个问题)。
原因是运行的是debug,但是flutter framework的物料是release的。
上面接入的时候提到过,这里官网上是引入Release目录下的文件,但是我先引用的是Debug目录下的,就是因为这个问题。但是如果已经按照官网引入release物料,就会出现上面的问题,这时候先清理一下项目
Product -> Clean Build Folder
然后在General下的FrameWorks, Libraries, and Embedded Content下将之前引入的文件移除掉,再重新引入Debug目录下的文件即可。再运行就可以正常展示flutter了。
当然,如果要运行release,则需要再执行上面的操作替换一下文件。这也是这种方案的最大弊端。
启动不同的flutter页面
上面我们只是启动flutter默认主页,可以看到在app启动时就将flutter engine启动起来,这样当我们点击按钮启动页面的时候,flutter页面很快就打开了。
当时如果启动不同的flutter页面怎么办?比如有两个按钮,分别启动flutter的主页面和second页面。参考官方文档,可以使用隐式flutter engine来启动,将ViewController的代码修改如下:
import UIKit import Flutter class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let button1 = UIButton(type:UIButton.ButtonType.custom) button1.addTarget(self, action: #selector(showMain), for: .touchUpInside) button1.setTitle("show main!", for: UIControl.State.normal) button1.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0) button1.backgroundColor = UIColor.blue self.view.addSubview(button1) let button2 = UIButton(type:UIButton.ButtonType.custom) button2.addTarget(self, action: #selector(showSecond), for: .touchUpInside) button2.setTitle("show second!", for: UIControl.State.normal) button2.frame = CGRect(x: 80.0, y: 310.0, width: 160.0, height: 40.0) button2.backgroundColor = UIColor.blue self.view.addSubview(button2) } @objc func showMain() { let flutterViewController = FlutterViewController(project: nil, initialRoute: "/", nibName: nil, bundle: nil); present(flutterViewController, animated: true, completion: nil) } @objc func showSecond() { let flutterViewController = FlutterViewController(project: nil, initialRoute: "second", nibName: nil, bundle: nil); present(flutterViewController, animated: true, completion: nil) } } 复制代码
这样就可以启动不同的页面,但是可以发现我们没有用到之前在AppDelegate创建的flutterEngine,因为创建FlutterViewController时都会隐式的创建新的flutterEngine,这也导致了一个问题,每次启动页面都需要等待一段时间。
我们可以预先创建两个flutterEngine,AppDelegate代码修改如下:
import UIKit import Flutter @UIApplicationMain class AppDelegate: FlutterAppDelegate { lazy var flutterEngine1 = FlutterEngine(name: "main") lazy var flutterEngine2 = FlutterEngine(name: "second") override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. flutterEngine1.run(withEntrypoint: "main", initialRoute: "/") flutterEngine2.run(withEntrypoint: "main", initialRoute: "second") return true } // MARK: UISceneSession Lifecycle override func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } override func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } } 复制代码
然后修改ViewController的代码如下:
import UIKit import Flutter class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let button1 = UIButton(type:UIButton.ButtonType.custom) button1.addTarget(self, action: #selector(showMain), for: .touchUpInside) button1.setTitle("show main!", for: UIControl.State.normal) button1.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0) button1.backgroundColor = UIColor.blue self.view.addSubview(button1) let button2 = UIButton(type:UIButton.ButtonType.custom) button2.addTarget(self, action: #selector(showSecond), for: .touchUpInside) button2.setTitle("show second!", for: UIControl.State.normal) button2.frame = CGRect(x: 80.0, y: 310.0, width: 160.0, height: 40.0) button2.backgroundColor = UIColor.blue self.view.addSubview(button2) } @objc func showMain() { let flutterEngine1 = (UIApplication.shared.delegate as! AppDelegate).flutterEngine1 let flutterViewController = FlutterViewController(engine: flutterEngine1, nibName: nil, bundle: nil) present(flutterViewController, animated: true, completion: nil) } @objc func showSecond() { let flutterEngine2 = (UIApplication.shared.delegate as! AppDelegate).flutterEngine2 let flutterViewController = FlutterViewController(engine: flutterEngine2, nibName: nil, bundle: nil) present(flutterViewController, animated: true, completion: nil) } } 复制代码
这样再启动页面就会瞬间打开了,因为flutterEngine已经提前启动起来了。
#####Undefined symbol: _FlutterDefaultDartEntrypoint
过程中出现过一个问题,一开始启动flutterEngine的代码是根据官网上的写法如下:
flutterEngine.run(withEntrypoint: FlutterDefaultDartEntrypoint, initialRoute: FlutterDefaultInitialRoute) 复制代码
这样可以启动flutter的默认页面。但是编译报错:
Undefined symbol: _FlutterDefaultDartEntrypoint
Undefined symbol: _FlutterDefaultInitialRoute
在FlutterEngine.h源码下可以看到对应的变量,但是通过在Debug/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework下的Flutter文件(C文件生成的二进制文件)中搜索发现并没有这两个字段,说明在C文件中并没有定义这两个字段。
目前还不确定是不是flutter编译导致的问题。但是我们可以解决这个问题,首先
FlutterDefaultInitialRoute就是默认路径,其实就是"/"。而FlutterDefaultDartEntrypoint就是默认入口,就是flutter中的main函数,所以就是"main"。所以在上面代码中我直接使用了这两个字符串来代替这两个字段。
总结
所以我们现在面临着与Android同样的困境,需要解决两个问题:
1、不支持传参数
2、每一个页面都需要一个flutterEngine,所以每加一个flutter页面就需要在ios代码中新增一个flutterEngine
所以我们一样需要用一个类似闲鱼flutter-boost原理的框架来管理flutter页面,下一步我会开发一个简单的快速启动框架。