美文网首页flutter相关
Flutter与原生混合开发

Flutter与原生混合开发

作者: iOS小孟和小梦 | 来源:发表于2021-12-30 16:16 被阅读0次

    一、背景

    • 背景:介于目前Flutter的学习进度已经告一段落, 但是如果直接使用flutter重写已有的app是不现实的, 因此需要调研Flutter嵌入原生app项目的技术手段
    • 技术定位:中级
    • 技术应用场景:Android/iOS 已有原生app
    • 整体思路:根据官方文档 Add-to-App 提供的方案: 将Flutter打包成library/module, 然后导入到对应项目中, 当做一个三方库来使用
    • 其他: 官方已提供了混合开发的demo, 可以参考一下

    二、操作步骤

    2.1 开发前的准备工作

    准备工作

    • 熟悉Flutter基本开发
    • 熟悉对应原生平台项目开发及对library/module的操作

    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 两种方式优劣对比

    • 第一种方式可以更方便运行时修改问题,但是对主项目“污染”会比较高,同时改动会大一些。
    • 第二种方式 需要单独调试后,更新 aar 文件再集成到项目中调试,但是这类集成方式更干净,同时 Flutter 相关代码可独立运行测试,且改动较小。

    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 三种方式的优劣对比

    • 第一种方式是官方推荐
    • 优点是便于操作, 一步到位, 也比较规范
    • 缺点是需要每个开发项目的人都配置flutter环境; 引用的framework会分布在不同的文件夹中, 查看比较繁琐
    • 第二种方式 需要手动引入, 而且需要修改配置, 十分麻烦, 一般没人用
    • 第三种方式 相对第一种稍微复杂一点, 但是引入之后整个模块都是作为单独一个库, 查看不会很繁琐

    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),它有三种类型:

    • MethodChannel:用于最常见的方法传递,帮助Flutter和原生平台互相调用方法, 这次就直接用这个, 其他的后面再补充
    • BasicMessageChannel:用于数据信息的传递。
    • EventChannel:用于事件监听传递等场景
    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
    • 在Android Studio中直接点击 flutter attach 按钮
    • 还有VS Code , 方法也差不多


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

    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(预热引擎), 还能再优化点

    • 在 Android 上预热需要 42 MB 和 1530 毫秒。其中 330 毫秒是主线程上的阻塞调用。
    • 在 iOS 上预热需要 22 MB 和 860 毫秒。其中 260 毫秒是主线程上的阻塞调用。

    2.4 存在的问题

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

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

    • 以今日头条为代表的修改 Flutter Engine 源码,使多 FlutterView 实例对应的多 Flutter Engine 能够在底层共享 Isolate;
    • 以闲鱼为代表的共享 FlutterView,即由原生层驱动 Flutter 层渲染内容的方案。

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

    相关文章

      网友评论

        本文标题:Flutter与原生混合开发

        本文链接:https://www.haomeiwen.com/subject/qqtjoltx.html