<简书 — 刘小壮> https://www.jianshu.com/p/d27c1f5ee3ff
封面图
iOS接入Flutter
在进行iOS
和Flutter
的混编时,iOS
比Android
的接入方式略复杂,但也还好。现在市面上有不少接入Flutter
的方案,但大多数都是千篇一律相互抄的,没什么意义。
进行Flutter
混编之前,有一些必要的文件。
xcode_backend.sh
文件,在配置flutter
环境的时候由Flutter
工具包提供。xcconfig
环境变量文件,在Flutter
工程中自动生成,每个工程都不一样。
xcconfig文件
xcconfig
是Xcode
的配置文件,Flutter
在里面配置了一些基本信息和路径,接入Flutter
前需要先将xcconfig
接入进来,否则一些路径等信息将会出错或找不到。
Flutter
的xcconfig
包含三个文件,Debug.xcconfig
、Release.xcconfig
、Generated.xcconfig
,需要将这些文件配置在下面的位置,并且按照不同环境配置不同的文件。
Project -> Info -> Development Target -> Configurations
系统设置
有些比较大的工程中已经在Configurations
中设置了xcconfig
文件,由于每个Target
的一种环境只能配置一个xcconfig
文件,所以可以在已有的xcconfig
文件中import
引入Generated.xcconfig
文件,并且不需要区分环境。
脚本文件
xcode_backend.sh
脚本文件用来构建和导出Flutter
产物,这是Flutter
开发包为我们默认提供的。需要在工程Target
的Build Phases
加入一个Run Script
文件,并将下面的脚本代码粘贴进去。需要注意的是,不要忘记前面的/bin/sh
操作,否则会导致权限错误。
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build /bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
在xcode_backend.sh
中有三个参数类型,build
、thin
、embed
,thin
没有太大意义,其他两个则负责构建和导出。
混合开发
随后可以对Xcode
工程进行编译,这时候肯定会报错的。但是不要慌张,报错后我们在工程主目录下会发现一个名为Flutter
的文件夹,其中会包含两个framework
,这个文件夹就是Flutter
的编译产物,我们将这个文件夹整体拖入项目中即可。
这时候就可以在iOS
工程中添加Flutter
代码了,下面是详细步骤。
将
AppDelegate
的集成改为FlutterAppDelegate
,并且需要遵循FlutterAppLifeCycleProvider
代理。
#import <Flutter/Flutter.h>#import <UIKit/UIKit.h>@interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider>@end
创建一个
FlutterPluginAppLifeCycleDelegate
的实例对象,这个对象负责管理Flutter
的生命周期,并从Platform
侧接收AppDelegate
的事件。我直接将其声明为一个属性,在AppDelegate
中的各个方法中,调用其方法进行中转操作。
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions]; return YES; } - (void)applicationWillResignActive:(UIApplication *)application { [self.lifeCycleDelegate applicationWillResignActive:application]; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { [self.lifeCycleDelegate application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; return YES; }
随后即可加入
Flutter
代码,加入的方式也很简单,直接实例化一个FlutterViewController
控制器即可,也不需要传其他参数进去(这里先不考虑多实例的问题)。
FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
Flutter
将其看做是一个画布,实例化一个画布上去之后,任何操作其实都是在当前页面完成的。
常见错误
到这个步骤集成操作就已经完成,但是很多人在集成过程中会遇到一些错误,下面是一些常见错误。
路径错误,读取不到
xcode_backend.sh
文件等。这是因为环境变量FLUTTER_ROOT
没有获取到,FLUTTER_ROOT
配置在Generated.xcconfig
中,可以看一下这个文件是不是配置的有问题。lipo info *** arm64
类似这样的错误,一般都是因为xcode_backend.sh
脚本导致的,可以检查一下FLUTTER_ROOT
环境变量是否正确。下面这种问题一般都是因为权限导致的,可以查看
Build Phases
的脚本写的是不是有问题。
***/flutter_tools/bin/xcode_backend.sh: Permission denied
混合开发
在进行混编过程中,Flutter
有一个很大的优势,就是如果Flutter
代码出问题,不会导致原生应用的崩溃。当Flutter
代码出现崩溃时,会在屏幕上显示错误信息。
在开发过程中经常会涉及到网络请求和持久化的问题,如果混编的话可能会涉及到写两套逻辑。例如网络请求有一些公共参数,或返回数据的统一处理等,如果维护两套逻辑的话会容易出问题。所以建议将网络请求和持久化操作都交给Platform
处理,Flutter
侧只负责向Platform
请求并拿来使用即可。
这个过程就涉及到两端数据交互的问题,Flutter
对于混编给出了两套方案,MethodChannel
和EventChannel
。从名字上来看,一个是方法调用,另一个是事件传递。但实际开发过程中,只需要使用MethodChannel
即可完成所有需求。
Flutter to Native
下面是Flutter
调用Native
的代码,在Native
中通过FlutterMethodChannel
设置指定的回调代码,并且在接收参数并处理。由Flutter
通过MethodChannel
对Native
发起调用,并传入对应的参数。
代码中在Flutter
侧构建好数据模型,然后调用MethodChannel
的invokeMethod
,会触发Native
的回调。Native
拿到Flutter
传过来的数据,进行解析并执行播放操作,随后会把播放的状态码回调给Flutter
侧,交互完成。
import 'package:flutter/services.dart'; Future<Null> playVideo() async{ var methodChannel = MethodChannel('flutterChannelName'); Map params = {'playID' : '302998298', 'duration' : '2520', 'name' : '三生三世十里桃花'}; String result; result = await methodChannel.invokeMethod('PlayAlbumVideo', params); String playID = params['playID']; String duration = params['duration']; String name = params['name']; showCupertinoDialog(context: context, builder: (BuildContext context){ return CupertinoAlertDialog( title: Text(result), content: Text('name:$name playID:$playID duration:$duration'), actions: <Widget>[ FlatButton( child: Text('确定'), onPressed: (){ Navigator.pop(context); }, ) ], ); }); }
NSString *channelName = @"flutterChannelName"; FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterVC]; [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { if ([call.method isEqualToString:@"PlayAlbumVideo"]) { NSDictionary *params = call.arguments; VideoPlayerModel *model = [[VideoPlayerModel alloc] init]; model.playID = [params stringForKey:@"playID"]; model.duration = [params stringForKey:@"duration"]; model.name = [params stringForKey:@"name"]; NSString *playStatus = [SVHistoryPlayUtil playVideoWithModel:model showPlayerVC:self.flutterVC]; result([NSString stringWithFormat:@"播放状态 %@", playStatus]); } }];
Native to Flutter
Native
调用Flutter
的代码和Flutter
调用Native
的基本类似,只是调用和设置回调的角色不同。同样的,Flutter
由于要接收Native
的消息回调,所以需要注册一个回调,由Native
发起对Flutter
的调用并传入参数。
Native
和Flutter
的相互调用都需要设置一个名字,每一个名字对应一个MethodChannel
对象,每一个对象可以发起多次调用,不同调用以invokeMethod
做区分。
import 'package:flutter/services.dart';@overridevoid initState() { super.initState(); MethodChannel methodChannel = MethodChannel('nativeChannelName'); methodChannel.setMethodCallHandler(callbackHandler); } Future<dynamic> callbackHandler(MethodCall call) { if(call.method == 'requestHomeData') { String title = call.arguments['title']; String content = call.arguments['content']; showCupertinoDialog(context: context, builder: (BuildContext context){ return CupertinoAlertDialog( title: Text(title), content: Text(content), actions: <Widget>[ FlatButton( child: Text('确定'), onPressed: (){ Navigator.pop(context); }, ) ], ); }); } }
NSString *channelName = @"nativeChannelName"; FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterVC]; [RequestManager requestWithURL:url success:^(NSDictionary *result) { [methodChannel invokeMethod:@"requestHomeData" arguments:result]; }];
调试工具集
在iOS
和Android
开发中,各自的编译器都提供了很好的调试工具集,方便进行内存、性能、视图等调试。Flutter
也提供了调试工具和命令,下面基于VSCode
编译器来讲一下Flutter
调试,相对而言Android Studio
提供的调试功能可能会更多一些。
性能调试
VSCode
支持一些简单的命令行调试指令,在程序运行过程中,在Command Palette
命令行面板中输入performance
,并选择Toggle Performance Overlay
命令即可。此命令有一个要求就是需要App在运行状态。
性能调试
随后会在界面上出现一个性能面板,这个页面分为两部分,GPU线程和UI线程的帧率。每个部分分为三个横线,代表着不同的卡顿层级。如果是绿色则表示不会影响界面渲染,如果是红色则有可能会影响界面的流畅性。如果出现红色线条,则表示当前执行的代码需要优化。
Dart DevTools
VSCode
为Flutter
提供了一套调试工具集-Dart DevTools
,这套工具集功能非常全,包含性能、UI、热更新、热重载、log日志等很多功能。
安装Dart DevTools
后,在App运行状态下,可以在VSCode
的右下角启动这个工具,工具会以网页的形式展现,并且可以控制App。
主界面
下面是Dart DevTools
的主界面,我运行的是一个界面类似于微信的App。从Inspector
中可以看到页面的视图结构,Android Studio
也有类似的功能。页面整体是一个树形结构,并且选中某一个控件后,会在右侧展示出控件的变量值,例如frame
、color
等,这个功能非常实用。
Dart DevTools
我运行的设备是Xcode
模拟器,如果想切换Android
的Material Design
,点击上面的iOS
按钮即可直接切换设备。刚才上面说到的查看内存的性能面板,点击iOS
按钮旁边的Performance Overlay
即可出现。
Select Widget
如果想知道在Dart DevTools
中选择的节点,具体对应哪个控件,可以选择Select Widget Mode
使屏幕上被选中的控件高亮。
Select Widget Mode
Debug Paint
点击Debug Paint
可以让每个控件都高亮,通过这个模式可以看到ListView
的滑动方向,以及每个控件的大小及控件之间的距离。
Debug Paint
除此之外,还可以选择Paint Baseline
使所有控件的底线高亮,功能和Debug Paint
类似,不做叙述。
Memory
Dart DevTools
中提供的内存调试工具更加直观,可以实时显示内存使用情况。在刚开始运行时,我们发现一个内存峰值,把鼠标放上去可以看到具体的内存使用情况。内存会有具体分类,Used
、GC
等。
Memory
Dart DevTools
的内存工具还是不够完美,Xcode
可以选择某段内存,看到这块内存中涉及到主要堆栈调用,并且点击调用栈可以跳转到Xcode
对应的代码中,而Dart DevTools
还不具备这个功能,可能和Web
的展示形式有关系。
内存管理Flutter
使用的是GC
,回收速度可能不是很快,iOS
中的ARC
则是基于引用计数立即回收的。还有很多其他的功能,这里就不一一详细叙述了,各位同学可以自己探索。
多实例
项目中是通过实例化FlutterViewController
控制器来显示Flutter
界面的,整个Flutter
页面可以理解为一个画布,通过页面不断的变化,改变画布上的东西。所以,在单实例的情况下,Flutter
页面中间不能插入原生页面。
这时候如果我们想在多个地方展示Flutter
页面,而这些页面并不是Flutter -> Flutter
的连贯跳转形式,那怎么来实现这个场景呢?Google
的建议是创建Flutter
的多实例,并通过传入不同的参数实例化不同的页面。但这样会造成很严重的内存问题,所以并不能这么做。
Router
如果不能真正创建多个实例对象,那就需要通过其他方式来实现多实例。Flutter
页面显示其实并不是跟着FlutterVC
走的,而是跟着FlutterEngine
走的。所以在创建一次FlutterVC
之后,就将FlutterEngine
保存下来,在其他位置创建FlutterVC
时直接通过FlutterEngine
的方式创建,并且在创建后进行跳转操作。
在进行页面切换时,通过channelMethod
调用Flutter
侧的路由切换代码,并将切换后的新页面FlutterVC
添加到Native
上。这种实现方式,就是通过Flutter
的Router
的方式实现的,下面将会介绍Router
的两种表现形式,静态路由和动态路由。
静态路由
静态路由是MaterialApp
提供的一个API
,routes
本质上是一个Map
对象,其组成结构是key
是调用页面的唯一标识符,value
就是对应页面的Widget
。
在定义静态路由时,可以在创建Widget
时传入参数,例如实例化ContactWidget
时就可以传入对应的参数过去。
void main() { runApp( MaterialApp( home: Page2(), routes: { 'page1': (_) => Page1(), 'page2': (_) => Page2() }, ), ); }class Page1 extends StatelessWidget { @override Widget build(BuildContext context) { return ContactWidget(); } }class Page2 extends StatelessWidget { @override Widget build(BuildContext context) { return HomeScreen(); } }
进行页面跳转时,通过Navigator
进行调用,每次调用都会重新创建对应的Widget
。进行调用时pushNamed
函数会传入一个参数,这个参数就是定义Map
时对应页面的key
。
Navigator.of(context).pushNamed('page1');
动态路由
静态路由的方式并不是很灵活,相对而言动态路由更加灵活。动态路由不需要预先设定routes
,直接调用即可。和普通push
不同的是,动态路由在push
时通过PageRouteBuilder
来构建push
对象,在Builder
的构建方法中执行对应的页面跳转操作即可。
结合之前说的channelMethod
,就是在channelMethod
对应的Callback
回调中,执行Navigator
的push
函数,接收Native
传递过来的参数并构建对应的Widget
页面,将Widget
返回给Builder
即可完成页面跳转操作。所以说动态路由的方式非常灵活。
无论是通过静态路由还是动态路由的方式创建,都可以通过then
函数接收新页面返回时的返回值。
Navigator.of(context).push(PageRouteBuilder( pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return ContactWidget('next page value'); } transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { return FadeTransition( child: child, opacity: animation, ); } )).then((onValue){ print('pop的返回值 $onValue'); });
但动态路由的跳转方式也有一些问题,会导致动画失效。所以需要重写Builder
的transitionsBuilder
函数,来自定义转场动画。
无论是通过静态路由还是动态路由的方式创建,都会存在一些问题。由于每次都是新创建Widget
,所以在创建时会有黑屏的问题。而且每次创建的话,都会丢失当前页面上次的上下文状态,每次进来都是一个新页面。
简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我Github
上,下载Flutter编程指南 PDF
合集。把所有Flutter
文章总计三篇,都写在这个PDF
中,而且左侧有目录,方便阅读。
Flutter编程指南
下载地址:Flutter编程指南 PDF
麻烦各位大佬点个赞,谢谢!?