flutter相关

Flutter与原生混合开发

2021-12-30  本文已影响0人  iOS小孟和小梦

一、背景

二、操作步骤

2.1 开发前的准备工作

准备工作

2.2 进入开发阶段

2.2.1 导入到Android 项目

Flutter引入Android有两种方式: 作为源代码 Gradle 子项目或 AAR 嵌入。

  1. 注意Flutter目前支持的架构: Flutter 目前仅支持为 x86_64、armeabi-v7a 和 arm64-v8a 构建提前 (AOT) 编译库, 如果Android项目支持别的可能要去掉
  2. 要注意flutter支持的gradle版本, 比如

2.2.1.1 利用AS创建或导入flutter模块, 直接依赖源代码

直接在AS中选择File > New > New Module, 就能直接创建或者导入Flutter模块, 然后就可以了(不要太简单)!

Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.002.png

2.2.1.2 手动编译引入

手动编译引入有两种方式

2.2.1.2.1 通过命令行完成

  1. 先在命令行创建Flutter模块(注意包名不要和主项目相同)
flutter create -t module --org com.example test_module

生成的目录结构如下(注意不需要修改.android文件夹内的内容, 这个是每次运行flutter pub get 就会自动生成的, 修改了也没用)

Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.003.png
  1. 在引入之前注意要在工程的 build.gradle 文件中添加配置
android {
  compileOptions {
    sourceCompatibility 1.8
    targetCompatibility 1.8
  }
}
  1. 开始导入 , 在flutter模块的根目录下运行
flutter build aar
  1. 命令会在build文件夹里面生成各种环境的包, 并且此时命令行会提示如何继承进原生工程, 按照提示修改对应文件即可
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.004.png Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.005.png

2.2.1.2.2 项目直接依赖模块源代码

  1. 前面1-2 步骤还是一样要通过命令行创建模块
  2. 在主项目的settings.gradle 文件中包含模块代码, 然后同步一下
setBinding(new Binding([gradle: this]))                                // new
evaluate(new File(                                                     // new
  settingsDir.parentFile,                                              // new
  '../xx/test_module/.android/include_flutter.groovy'                  // new
))                                                                     // new
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.006.png
  1. 最后在build.gradle中导入flutter模块就完成导入了
dependencies {
  implementation project(':flutter')
}

2.2.1.3 两种方式优劣对比

2.2.1.4 测试代码

直接运行可能会报错, 需要修改android/build.gradle

buildscript {
    repositories {
//        google()
//        jcenter()
        maven {
            url 'https://maven.aliyun.com/repository/google' }
        maven {
            url 'https://maven.aliyun.com/repository/jcenter' }
        maven {
            url 'https://maven.aliyun.com/nexus/content/groups/public' }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.0'
    }
}

修改settings.gradle

repositoriesMode.set(RepositoriesMode. FAIL_ON_PROJECT_REPOS)
改为
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)

编译没有报错之后就可以开始写测试代码

1. AndroidManifest.xml 中添加 activity 
<activity
    android:name="io.flutter.embedding.android.FlutterActivity"
    android:theme="@style/Theme.AppCompat.DayNight"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
    android:hardwareAccelerated="true"
    android:windowSoftInputMode="adjustResize"
    />
   
2. MainActivity中添加展示代码
void showView(){
    startActivity(FlutterActivity.createDefaultIntent(this));
}

2.2.1.5 Flutter与Android的通信

Flutter与Android原生交互有专门的通信对象(MethodChannel

  1. 想要接收和发送消息, 首先要定义消息通道的唯一id, 并且在两边使用相同的id
//Flutter向Native发消息 
private static final String CHANNEL_NATIVE = "com.example.flutter/native"; 
//Native向Flutter发消息 
private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";
  1. Android中代码
  2. 接收消息
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_NATIVE); 
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
     @Override
      public void onMethodCall(MethodCall call, MethodChannel.Result result) { 
          switch (call.method){ 
              case "方法名": 
                  result.success("收到来自Flutter的消息"); 
                  break;
              default : 
                  result.notImplemented(); 
                  break;
                 } 
                 //通过result告诉flutter处理记过
                 //result.success / result.notImplemented
       } 
});
  1. 发送消息
Map<String, Object> result = new HashMap<>(); 
result.put("message", @"消息内容"); //参数字段需要统一
MethodChannel flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_FLUTTER); // 调用Flutter端定义的方法 flutterChannel.invokeMethod("方法名", result);
  1. Flutter中代码
  2. 接收消息
Future<dynamic> handler(MethodCall call) async{
  switch (call.method){
    case '方法名':
      onDataChange(call.arguments['message']);
      break;
  }
}

flutterChannel.setMethodCallHandler(handler);
  1. 发送消息
 Map<String, dynamic> para = {'message':'传递的参数'};  //参数字段需要统一
 final String result = await channel.invokeMethod('方法名',para); 
 print('这是在flutter中打印的'+ result); 

2.2.1.6 通信过程出现的问题

//如果展示FlutterActivity和注册监听flutter消息的时候不是使用同一个引擎缓存可能会导致无法接收flutter消息
//如果要正常接收消息的话
1. 在OnCreate中创建和注册引擎
flutterengine = new FlutterEngine(this);
//预热引擎
flutterengine.getDartExecutor().executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());
//缓存 FlutterActivity 使用的 FlutterEngine
FlutterEngineCache.getInstance().put("my_engine_id" , flutterengine);

2. 展示flutterActivity时使用缓存的引擎来展示  注意id需要相同
startActivity(FlutterActivity.withCachedEngine("my_engine_id").build(this));

2.2.2 导入到iOS项目

2.2.2.1 创建Flutter模块(这里用test_module做示例)

flutter create --template module test_module

生成的目录结构如下(注意不需要修改.ios文件夹内的内容, 这个是每次运行flutter pub get 就会自动生成的, 修改了也没用)

Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.007.png

添加/编写代码到lib文件夹中, 添加需要依赖的插件到pubspec.yaml中, 然后运行 flutter pub get

2.2.2.2 生成Flutter库并引入到项目中

将Flutter module编译成framework, 引入iOS工程, 有三种方式

  1. 通过CocoaPods脚本自动引入
  2. 在iOS工程的profile配置文件中添加
flutter_application_path = '../Module/test_module' #注意这里需要使用相对路径
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.008.png
  1. 然后给工程中每一个需要嵌入framework的target的调用install

install_all_flutter_pods(flutter_application_path)

  1. 最后在项目目录下执行 pod install 即可完成嵌入(注意: 每次修改了yaml文件之后都需要执行 flutter pub get 和 重新pod install)
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.009.png
  1. 将Flutter Module编译产物通过本地引入工程
  2. 在flutter项目根目录下运行命令导出为framework
flutter build ios-framework --output=export/
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.010.png
  1. 得到framework之后 , 就跟本地直接引入framework一样 , 通过在Build Settings > Build Phases > Embed Frameworks中引入, 然后在Framework Search Paths添加$(PROJECT_DIR)/export/Release/
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.011.png

ng)

  1. 由于导出的framework是区分不同环境的, 所以需要配置修改引入路径为配置
在project.pbxproj中把(每一个framework路径都要改)
path = export/Release/xxx.xcframework;
替换为
path = "export/$(CONFIGURATION)/xxx.xcframework
  1. 然后把 Framework Search Paths 改为(PROJECT\_DIR)/export/(CONFIGURATION)
  2. 将编译产物通过CocoaPods引入
  3. Flutter 项目根目录下运行(多了个cocoapods参数)
flutter build ios-framework --cocoapods --output=export/
  1. 得到的文件夹实际上是多了一个cocoapods的配置文件


    Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.012.png
  2. 这里有两种引入方式选择

  3. 本地相对路径引入

  4. 把这个库封装成一个pod库, 上传到公开的cocoapods索引库或者自己的私有索引库

这里我们直接用本地化的就好了 直接在podfile文件中加入依赖并在根目录下运行 pod install

  1. 注意生成的文件夹里面的App.framework + FlutterPuginRegistrant.framwrok + shared_preferences.framework 还是与第二种方式相同的, 需要手动嵌入工程中
pod 'Flutter', :podspec => '../export/Debug/Flutter.podspec'
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.013.png

2.2.2.3 三种方式的优劣对比

2.2.2.4 优化流程

除了官方文档提供的方式, 还有另外一种方式可以引入, 并且可以利用脚本简化流程实现一键引入

  1. 直接在flutter根目录下运行命令 编程出产物, 实际上也是一堆framework
flutter build ios --${packageType} --no-codesign
  1. 命令行用pod命令新建一个pod组件
  2. 把编程产物收集到pod同一个目录下, 并修改podspec文件
  3. 然后利用cocoapods的本地引入, 把所有的framework封装为一个pod组件引入项目
    只需要提前建好pod组件, 修改好podspec文件以及项目podfile文件, 其余交给脚本就可以了
      
#前提flutter一定要是app项目: pubspec.yaml里 不要加
#module:
#  androidPackage: com.example.myflutter
#  iosBundleIdentifier: com.example.myFlutter

packageType='debug'
packageFileName='Debug'

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 --${packageType} --no-codesign

echo "编译flutter完成"
mkdir $out

cp -r build/ios/${packageFileName}-iphoneos/*/*.framework $out
cp -r build/ios/${packageFileName}-iphoneos/App.framework $out


# 这里不能使用build里面的flutter.framework , 里面缺少类
cp -r .ios/Flutter/engine/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework $out


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

libpath='../flutter_lib/flutter_lib/'

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

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

2.2.2.5 测试代码

引入库之后, 在iOS中导入头文件 #import <Flutter/Flutter.h> 然后编写跳转页面代码即可展示flutter页面

- (void)showFlutterView{
  //初始化FlutterViewController
  self.flutterViewController = [[FlutterViewController alloc] init];
  //为FlutterViewController指定路由以及路由携带的参数
  //设置模态跳转满屏显示
  self.flutterViewController.modalPresentationStyle = UIModalPresentationFullScreen;
  [self presentViewController:self.flutterViewController animated:YES completion:nil];
}
Aspose.Words.47babac3-0166-4cc3-9d57-f88474c3fb9b.014.png

2.2.2.6 Flutter与iOS的通信

FlutteriOS原生交互也有专门的通信对象(Platform Channel),它有三种类型:

  1. 想要接收和发送消息, 首先要定义消息通道的唯一id, 并且在两边使用相同的id
iOS中定义
//Flutter向Native发消息
static NSString *CHANNEL_NATIVE = @"com.example.flutter/native";
//Native向Flutter发消息
static NSString *CHANNEL_FLUTTER = @"com.example.flutter/flutter";


flutter中定义
static const nativeChannel = const MethodChannel('com.example.flutter/native');
static const flutterChannel = const MethodChannel('com.example.flutter/flutter');
  1. iOS中代码
  2. 接收消息
  //监听flutter的消息  这里需要绑定对应的flutterViewController中的binaryMessenger
  FlutterMethodChannel *messageChannel = [FlutterMethodChannel methodChannelWithName:CHANNEL_NATIVE binaryMessenger:self.flutterViewController.binaryMessenger];
  
  __weak typeof(self) weakSelf = self;
  //接受Flutter回调
  [messageChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if ([call.method isEqualToString:@"方法名"]) {
    //flutter 传递的参数  字段需要统一
        NSString *message = call.arguments[@"message"];
        NSLog(@"原生处理数据");
      
      //告诉Flutter我们的处理结果
      if (result) {
        result(@"xxxxx");
      }
    }
  }];
  1. 发送消息
//发送消息给flutter页面
  FlutterMethodChannel *messageChannel = [FlutterMethodChannel methodChannelWithName:CHANNEL_FLUTTER binaryMessenger:self.flutterViewController.binaryMessenger];
  [messageChannel invokeMethod:@"方法名" arguments:@{@"message" : message}]; //传递的参数字段需要统一
  1. flutter中代码
  2. 接收消息
Future<dynamic> handler(MethodCall call) async{
  switch (call.method){
    case '方法名':
      onDataChange(call.arguments['message']);
      break;
  }
}

flutterChannel.setMethodCallHandler(handler);
  1. 发送消息
Map<String, dynamic> para = {'message':'flutter 给原生的数据'};
final String result = await channel.invokeMethod('方法名',para);
print('原生返回的数据 ' + result);

2.2.4 Debug和热更新

flutter页面要进行热更新需要利用flutter attach , 它可以在任意途径启动(在app启动前启动后都可以)

flutter attach 或者 flutter attach -d deviceId

2.2.5 原生页面嵌入Flutter

前面使用的测试代码都是将flutter作为一整个页面引入, 而不是作为原生页面其中的某个视图, 这一节探索如何将flutter作为一个视图引入到原生页面中

2.2.5.1 Flutter在iOS中引入的方式都是通过FlutterViewController的方式, 如果要作为一个子View使用, 需要通过一些处理

  1. 首先FlutterViewController的初始化不能直接使用[[FlutterViewController alloc] init], 这种方式创建出来的实例可能会共享内存, 并非不同的实例
  2. 将Controller的View加载出来
  flutterViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
   //利用presentViewController 展示控制器但是立即dismiss, 只是为了让View加载出来, 这样就可以取出view加载到当前的View上
  [self presentViewController:flutterViewController animated:NO completion:^{
    [self dismissViewControllerAnimated:NO completion:^{
      flutterViewController.view.frame = CGRectMake(50, 50, self.view.frame.size.width * 0.5, self.view.frame.size.height * 0.5);
      flutterViewController.view.backgroundColor = [UIColor whiteColor];
      [self.view addSubview:flutterViewController.view];
      [self addChildViewController:flutterViewController];
      [self.view bringSubviewToFront:flutterViewController.view];
    }];
  }];
  1. 特别需要注意的是 当前页面销毁的时候, 需要把使用的FlutterView相关资源一并销毁防止内存泄漏
   //iOS监听flutter的通道
   [evenChannal setStreamHandler:nil];
   evenChannal = nil;
   //iOS
   [messageChannel setMethodCallHandler:nil];
   messageChannel = nil;

//使用initWithProject创建出来的FlutterViewController每个实例自带一个engine
    //销毁控制器的engine对象
   [flutterViewController.engine destroyContext];

2.3 加载顺序、性能和内存

2.3.1 加载步骤

  1. 构建FlutterEngine , 在.apk/.ipa/.app中加载资源(图片、字体等)
  2. 加载 Flutter 库 , 引擎的共享库加载一次内存(共享的库, 多个进程也只会加载一次)
  3. Dart运行时机制管理dart代码的内存和并发性(每个应用程序都会存在一个Dart运行时 , 而且不会关闭)
  4. 在 Android 上第一次构建 FlutterEngine 和在 iOS 上第一次运行 Dart 入口点时,会完成一次 Dart VM 启动。
  5. Dart代码的快照会从程序文件加载到内存中, 这里会涉及到dart的JIT特性
  6. Dart运行时初始化后, 由Flutter引擎对管理dart运行时, 创建和运行Dart Isolate
  7. 将 UI 附加到 Flutter 引擎, 此时Flutter生成layer树会被转化为OpenGL(或者类似的绘图)指令

2.3.2 占用内存和延迟

Flutter的启动延迟还算是比较低的, 如果可以提前启动FlutterEngine(预热引擎), 还能再优化点

2.4 存在的问题

需要注意的是,与纯 Flutter 应用不同,原生应用混编 Flutter 由于涉及到原生页面与 Flutter 页面之间切换,因此导航栈内可能会出现多个 Flutter 容器的情况,即多个 Flutter 实例。Flutter 实例的初始化成本非常高昂,每启动一个 Flutter 实例,就会创建一套新的渲染机制,即 Flutter Engine,以及底层的 Isolate。而这些实例之间的内存是不互相共享的,会带来较大的系统资源消耗。

为了解决混编工程中 Flutter 多实例的问题,业界有两种解决方案:

不过,目前这两种解决方案都不够完美。所以,在 Flutter 官方支持多实例单引擎之前,应该尽量使用Flutter去开发一些闭环业务,减少原生页面与Flutter页面之间的交互,尽量避免Flutter页面跳转到原生页面,原生页面又启动一个新的Flutter实例的情况,并且保证应用内不要出现多个 Flutter 容器实例的情况。

上一篇 下一篇

猜你喜欢

热点阅读