共计 8983 个字符,预计需要花费 23 分钟才能阅读完成。
该文章属于 < 简书 — 刘小壮 > 原创,转载请注明:
< 简书 — 刘小壮 > 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';
@override
void 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
等,这个功能非常实用。
我运行的设备是 Xcode
模拟器,如果想切换 Android
的Material Design
,点击上面的 iOS
按钮即可直接切换设备。刚才上面说到的查看内存的性能面板,点击 iOS
按钮旁边的 Performance Overlay
即可出现。
Select Widget
如果想知道在 Dart DevTools
中选择的节点,具体对应哪个控件,可以选择 Select Widget Mode
使屏幕上被选中的控件高亮。
Debug Paint
点击 Debug Paint
可以让每个控件都高亮,通过这个模式可以看到 ListView
的滑动方向,以及每个控件的大小及控件之间的距离。
除此之外,还可以选择 Paint Baseline
使所有控件的底线高亮,功能和 Debug Paint
类似,不做叙述。
Memory
Dart DevTools
中提供的内存调试工具更加直观,可以实时显示内存使用情况。在刚开始运行时,我们发现一个内存峰值,把鼠标放上去可以看到具体的内存使用情况。内存会有具体分类,Used
、GC
等。
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
,所以在创建时会有黑屏的问题。而且每次创建的话,都会丢失当前页面上次的上下文状态,每次进来都是一个新页面。