likes
comments
collection
share

Flutter 性能优化之 FFI 系列(一): Flutter调用Go的库文件

作者站长头像
站长
· 阅读数 90

目录

  • 一、需求背景
  • 二、环境搭建
  • 三、Go 代码及编译
  • 四、Flutter 端代码和编译
  • 五、总结

一、需求背景

Flutter 本身定位更接近于UI框架,主要实现各平台上的UI交互的统一性。当需要调用各平台本身的功能时,通常会用 Channel 来调用原生方法。或者用 ffi 调用 C 的实现。 但这系列文章抛去以往的经验,改用 Flutter & Go 的的方式,用 Flutter 集成 Go 编译的so动态库和.a的静态库,来实现跨平台开发。

为什么是 Go ? 1.因为生态,尤其是在网络编程方面的生态 2.不会 C 和 C++ 😀😀😀

二、环境搭建

2.1 开发环境

我的开发环境如下,不同版本在编译和数据类型上会有些差别 Flutter、IOS、Android、Go 各自的开发环境搭建是基础,不再列出来。

  • 硬件设备 M1 Mac
  • Flutter 3.10.6
  • go version go1.20.3

2.2 Flutter 端创建

Flutter 端调用 native 更适合用插件模式提供给主业务调用,这样方便后期的维护。Flutter 创建插件 package 可以完全按照官网的文档来操作

  • 创建个 Flutter 的插件,并加入 Android 和 IOS 平台支持
flutter create --template=plugin --platforms=android,ios nativeflplug

因为 Go 也可以交叉编译成其他的平台动态库,如果有需要其他平台的,可以在--platforms参数继续添加,或者后续随时可以添加其他平台的支持,如添加 windows 平台支持

flutter create --template=plugin --platforms=windows .

之后执行命令 flutter pub getflutter run,尝试运行项目

2.3 Go 端创建

在项目目录下执行命令,将创建一个go的项目

go mod init native

创建个 main.go的文件,并编写个最基本的代码

package main  
  
import "fmt"  
  
func main() {  
    fmt.Printf("Hello, world.\n")  
}

执行 go build 命令编译 go 项目,并执行命令 ./native,尝试将这个 Go 的 Hello, world 运行起来

三、Go 代码及编译

3.1 CGO

CGO(C Go)是Go语言的一个工具,用于与C语言进行互操作。它允许Go代码调用C代码,并且可以使Go代码和C代码之间相互调用

Go 能被 flutter 调用,本质还是通过C与Flutter直接相互的调用,所以在我们的插件中, Go 的代码部分会有很多关于 C 语言的内容。 以下是一个最基本的 cgo 的代码,会输出一个 TestPlus 的方法

package main  
  
/*  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
*/  
import "C"  
  
//export TestPlus  
func TestPlus(a, b int) int {  
    return a + b  
}

  • import "C" Go语言中用于与C语言进行互操作的特殊导入语句。它的主要作用是引入C语言的符号和函数,使得Go代码可以与C代码进行互操作
  • #include <string.h> 不是实际的代码行,而是一种特殊注释格式,用于告诉cgo工具在生成C代码时包含C标准库的头文件 <string.h>。这个注释是cgo的一部分,而不是Go本身的语法。
  • //export TestPlus 是在使用Go的cgo工具时的一个特殊注释,用于告诉cgo工具将Go函数导出为C语言的可调用函数,以便C代码可以调用这些Go函数。

以上就是一个最基本的 go 的代码,可以编译成一个库给其他语言调用,这个库现在有一个 TestPlus 的方法。

3.2 交叉编译

Go语言的交叉编译(Cross-Compilation)是指在一个开发机器上构建可以在不同的操作系统和架构上运行的可执行文件。这允许开发者在一个平台上编写和构建Go代码,然后将生成的二进制文件部署到其他不同的平台上运行,而无需在每个目标平台上都进行编译。

3.2.1 编译成 Android 的动态库

设置环境变量,编译Android的so库,需要预先安装 NDK

export GOOS=android
export GOARCH=arm64 # 或者其他目标架构,根据你的需求
export CGO_ENABLED=1
export CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang # 根据你的架构和NDK版本修改
  • GOARCH=arm64 将编译成 arm64-v8a,也是现在Android手机保有量最大的cpu架构
  • CGO_ENABLED=1 CGO 支持
  • CC= ... 本地的 NDK 目录,根据自己的开发环境和电脑修改

执行编译命令

go build -buildmode=c-shared -o libnativeffi.so

编译后将生成 libnativeffi.so的动态库,和 libnativeffi.h的头文件。so动态库是提供给Android 加载调用的, .h 这个头文件定义了Go函数的声明和Go数据类型的结构,后续将在 Flutter 构建时用到。

3.2.2 编译成 IOS 的静态库

根据需要执行指定目标平台

  • 手机
CC=~/go/misc/ios/clangwrap.sh GOOS=ios GOARCH=arm64 CGO_ENABLED=1 
go build -buildmode=c-archive -o out/ios/ios/libnativeffi.a
  • x86模拟器
CC=~/go/misc/ios/clangwrap.sh GOOS=ios GOARCH=amd64 CGO_ENABLED=1 
go build -buildmode=c-archive -o out/ios/x86/libnativeffi.a

合并库文件

lipo -create out/ios/ios/libnativeffi.a out/ios/x86/libnativeffi.a 
-output libnativeffi.a
#查看生成文件的信息
lipo -info libnativeffi.a

最后执行 lipo -info查看生成的文件信息,根据不同目标平台,会显示如下:

Architectures in the fat file: out/ios/libnativeffi.a are: x86_64 arm64
  • arm64 即当前的手机架构和 m1 Mac CPU 的模式
  • x86_64 Intel CPU 的模拟器的支持

最后生成 libnativeffi.a 文件就是我们需要提供IOS平台的静态库。同时生成 libnativeffi.h的头文件,和Android端一样,都是描述库文件的头文件

3.2.3 其他平台

Go 支持多平台的交叉编译,Flutter 也支持跨平台的UI。如果需要 windows、macOS 等平台,修改编译命令,指定目标平台即可。

四、Flutter 端代码和编译

4.1 FFI

FFI是一种机制,通过该机制,用一种编程语言编写的程序可以调用例程或使用另一种编程语言编写或编译的服务。FFI通常用于调用二进制动态链接库的上下文中 - 来之百科

FFI 在不同语言上都有不同的实现,Flutter 中也有对应的 FFI 实现,Dart FFI 是Dart2.12.0版本后(同时包含在Flutter 2.0 和以后的版本里),才作为稳定版本发布,在pub.dev 的链接。

4.2 ffigen

一个Flutter工具,用于生成Flutter Dart代码,以便在Flutter应用程序中与C/C++代码进行交互。它可以自动化生成与C/C++代码的绑定,以便你可以在Dart代码中调用本地C/C++函数和访问本地库的数据结构。

这里 官方介绍,主要是为 dart 生成与头文件对应的调用方法

4.3 Flutter 配置

4.3.1 导入 ffi 和 ffigen 插件

在 Flutter 项目中 pubspec.yaml 的文件里,引入 ffi 和 ffigen

dependencies:
  ffi: ^2.1.0

dev_dependencies:
  ffigen: ^9.0.1

执行 flutter pub get 命令

4.3.2 配置 ffigen

同样在 Flutter 项目中 pubspec.yaml 的文件,配置 ffigen 的生成规则

ffigen:
  # 生成文件的路径
  output: lib/src/nativefl.dart
  # 生成文件的 class name
  name: Nativefl
  # 生成的描述
  description: Native bindings for nativefl
  headers:
    entry-points:
      # 头文件,根据库文件会生成具体的方法
      - libnativeffi.h
  • output ffigen 生成代码的文件路径
  • name 对应的 class name
  • entry-points 头文件位置,即 Go 交叉编译时生成的头文件,当前的配置是放到了项目的根目录。

执行 flutter pub run ffigen 命令,会自动在output目录下生成 dart文件,这个文件里包含了和我们编译的镜像文件里对应的方法

Flutter 性能优化之 FFI 系列(一): Flutter调用Go的库文件

如图中生成的 TestPlus 的 dart 方法,对应的就是 Go 中 TestPlus方法

4.3.3 FFI 常用方法

DynamicLibrary

  • DynamicLibrary.open 加载库函数,此方法用于加载动态库
DynamicLibrary.open('libnativeffi.so')
  • DynamicLibrary.process 用于加载系统进程中已加载的共享库。这意味着它可以用于访问系统库或其他已加载的库

  • DynamicLibrary.executable 属性用于加载与Flutter应用程序一起运行的可执行文件的共享库部分。这通常用于加载与Flutter应用程序一起打包的本地库

NativeType 用于表示与本地C/C++代码进行交互时的数据类型。它是一个抽象类,用于表示各种本地数据类型的Dart映射。NativeType的子类用于表示不同的本地数据类型,如有符号整数、无符号整数、指针等。例如,Int32表示一个32位有符号整数,Uint8表示一个8位无符号整数,IntPtr表示一个指针。

Pointer 用于与本地C/C++代码进行交互时,操作和管理指针类型的数据。Pointer类允许 Dart中创建、操作和传递指针

malloc 和 calloc 分配和释放内存

// 分配一个包含5个整数的内存块 
final Pointer<Int32> ptr = calloc<Int32>(5);
// 释放分配的内存块 
calloc.free(ptr);

4.3.4 Flutter 中 Android 配置

复制.so文件到指定目录 如下图,将 .so 文件复制到 libs/arm64-v8a/libnativeffi.so,如果有其他CPU机构支持,也一起复制,比如 libs/armeabi-v7a/libnativeffi.so

├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
│   ├── build.gradle
│   ├── libs
│   │   └── arm64-v8a
│   │       └── libnativeffi.so
│   ├── local.properties
│   ├── settings.gradle
│   └── src
│       ├── main
│       │   ├── AndroidManifest.xml
│       │   └── kotlin
├── example
├── ios
│   ├── Assets
│   ├── Classes
│   │   └── FlutterNativeGoPlugin.swift
│   ├── Frameworks
│   │   └── libnativeffi.a
│   └── flutter_native_go.podspec
├── lib
│   ├── flutter_native_go.dart
│   ├── flutter_native_go_method_channel.dart
│   ├── flutter_native_go_platform_interface.dart
│   └── src
│       ├── native_helper.dart
│       └── nativefl.dart
├── libnativeffi.h
├── pubspec.yaml

配置 Android 的构建文件 build.gradle

android {
   # 其他
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
        # 增加导入so的位置
        main.jniLibs.srcDirs = ['libs']
    }

    defaultConfig {
       # 增加cpu支
        ndk {
            abiFilters 'arm64-v8a'
        }
    }
    
}

4.3.5 IOS端配置

复制.a文件到 ios/Frameworks/libnativeffi.a 目录下 目录结构参考Android端

编写 .podspec 文件

CocoaPods 一个用于管理iOS项目中第三方库依赖的工具.podspec文件是CocoaPods的一个关键部分,用于描述和配置一个库或组件的信息,以便CocoaPods可以正确管理其安装和版本控制

在 .podspec 文件中增加如下

s.vendored_libraries = 'Frameworks/libnativeffi.a'
s.pod_target_xcconfig = { "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/Frameworks/libnativeffi.a" }
  • vendored_librariespodspec 文件中的一个选项,用于指定第三方库是否包含了预编译的二进制库(通常是 .a 文件)以及这些库的路径。
  • pod_target_xcconfig 是一个配置选项,允许你为特定的 CocoaPods 依赖项定义 Xcode 配置设置

4.3.6 Flutter 端实现调用 Go 方法

经过上面一系列的配置,现在我们的Flutter的代码中就可以正式访问 Go 提供的方法了。

加载动态库 根据不同的平台,应用不同的加载方式

static final DynamicLibrary _dylib = Platform.isAndroid ? DynamicLibrary.open('libnativeffi.so') : DynamicLibrary.process();

初始化 FFI

_nf = Nativefl(_dylib);

调用 FFI 提供的方法

int? testPlus(int a, int b) {
  var value = _nf?.TestPlus(a, b);
  return value;
}

经过以上的步骤,就可以在 Flutter 中通过预先加载的库文件,调用库文件里的方法。通常会将 FFI 中的方法封装处理一下再提供给业务层。因为FFI中很多方法涉及到 指针 内存 的操作,不适合在业务层处理。

Flutter 性能优化之 FFI 系列(一): Flutter调用Go的库文件

五、总结

通过以上的配置,最终会在 Flutter 中调用 Go 生成的库文件,依赖于 Go 丰富的生态可以完成很多复杂高性能的计算,让 Flutter 只专注于 UI 表现层,从而提供我们软件的性能。

以上只是最基本的配置、构建和编译,实现了 Flutter 调用 Go 代码中一个方法。后续将更多展开关于

  • 数据类型匹配 Dart 与 C 、Go 中各种数据类型的参数传递和返回,比如string、byte、数组、map 等。
  • 指针操作 通过指针访问本地内存,传递复杂数据等。
  • 内存管理 在通常Flutter开发中,并不涉及到内存管理。但在 FFI 的开发时,与 Native 交互中,内存管理是不得不面对的。
转载自:https://juejin.cn/post/7276269179073200183
评论
请登录