如何在 Flutter 中调用 C++ 代码
目录
- 需求背景
- FFI 简介
- FFI 实战 - OpenCv 高斯模糊
- FFI 中的指针与 C++ 中的指针
- 拓展:FFI 接收 C++ 中的结构体
- 总结
一、需求背景
如在 flutter 中调用 C/C++ 代码可以通过 method channel 的方式来调用 JNI 间接调用 C/C++ 代码 ,那么就会有较长的调用链 flutter -> java jni -> C/C++,
假如我们要传递一个参数,这个参数将会在 java、C++ 堆各拷贝一次,如果是大对象容易造成内存抖动,且效率较低。
那么 Dart 中有没有像 JNI 一样的东西,直接调用 C/C++ 代码?
引出我们今天的主角 FFI (我们是否可以叫它 DNI 呢?)
二、FFI 简介
FFI (Foreign function interface) 代表 外部功能接口,类似功能的其他术语包括本地接口和语言绑定。这个叫法延续在 Rust、Python、Dart等语言中,而 Java 将其 FFI 称为 JNI(Java 本机接口)。
2.1 Flutter 中的 FFI
在 Flutter2.0 中的 Dart 2.12 已发布,其中包含健全的空安全和 Dart FFI 的稳定版, 并且提供了一套类型绑定生成工具 ffigen,可以自动生成 Dart Wrapper 加快开发效率。
在目前最新的 Flutter 2.2 中,Dart 2.13 扩展了对原生互操作性的支持,现在支持在 FFI 中使用数组和封装结构体
可见 flutter 成为首选的多平台开发 UI 工具包之势日趋明显.
2.2 与 JNI 比较
网络上关于 FFI 的文章较少,我查阅到的是快手-开眼快创 Flutter 实践,相对于 JNI 它大大提升提升数据传输的性能。
使用
FFI后,首次加载缩略图速度提升2% ~ 16%,在涉及大量图片传输场景下数据提升明显,数据传输耗时占比较高,FFI替换Channel后传输耗时降低。
2.3 FFI 原理:如何找到 C++ 中的库
通过 DynamicLibrary.open() 查看源码实现
final DynamicLibrary ffiLib = Platform.isAndroid ? DynamicLibrary.open('lib_invoke.so') : DynamicLibrary.process();
DynamicLibrary.open()
最终执行的逻辑如下, 源码位于ffi_dynamic_library.cc:
static void* LoadExtensionLibrary(const char* library_file) {
#if defined(HOST_OS_LINUX) || defined(HOST_OS_MACOS) || \
defined(HOST_OS_ANDROID) || defined(HOST_OS_FUCHSIA)
void* handle = dlopen(library_file, RTLD_LAZY);
if (handle == nullptr) {
char* error = dlerror();
const String& msg = String::Handle(
String::NewFormatted("Failed to load dynamic library (%s)", error));
Exceptions::ThrowArgumentError(msg);
}
return handle;
……
可以看到最终使用 dlopen 加载动态链接库,并返回句柄。
拿到对应的动态链接库的句柄之后,就能使用相关方法进行操作了。
句柄主要包含以下两个方法:
// 在内存中查找对应符号名的地址,与dlsym()功能相同
external Pointer<T> lookup<T extends NativeType>(String symbolName);
// 1、去动态库中查找对应名称的函数
// 2、将 Native 类型的 C/C++ 函数转化为 Dart 的 Function 类型
external F lookupFunction<T extends Function, F extends Function>(String symbolName);
其中lookup()的最终实现主要使用了 dlsym
static void* ResolveSymbol(void* handle, const char* symbol) {
#if defined(HOST_OS_LINUX) || defined(HOST_OS_MACOS) ||
defined(HOST_OS_ANDROID) || defined(HOST_OS_FUCHSIA)
dlerror(); // Clear any errors.
void* pointer = dlsym(handle, symbol);
if (pointer == nullptr) {
char* error = dlerror();
const String& msg = String::Handle(
String::NewFormatted("Failed to lookup symbol (%s)", error));
Exceptions::ThrowArgumentError(msg);
}
return pointer;
dlopen:该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。这种机制使得在系统中添加或者删除一个模块时,都不需要重新进行编译。
dlsym:是一个计算机函数,功能是根据动态链接库操作句柄与符号,返回符号对应的地址,不但可以获取函数地址,也可以获取变量地址。 返回符号对应的地址。句柄与普通指针的区别:指针包含的是引用对象的内存地址,而句柄则是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。 这种间接访问对象的模式增强了系统对引用对象的控制。 句柄就是个数字,一般和当前系统下的整数的位数一样,比如
32bit系统下就是4个字节。 这个数字是一个对象的唯一标示,和对象一一对应
三、FFI 实战-OpenCv 高斯模糊
我们使用成熟的开源库 Android OpenCv SDK 在 Fluter 上实践 FFI 并实现高斯模糊。
左边是原图,右边是高斯模糊后的结果

3.1 环境搭建
使用 FFI 之前,必须首先确保本地代码已加载,并且其符号对 Dart 可见,然后才能在库或程序使用 FFI 库绑定本地代码。
3.1.1 下载 SDK
解压后包含以下内容:

3.1.2 导入文件
创建一个 flutter 插件名为 opencv_plugin,在 main 目录下新建 cpp 目录。
- 创建
native-lib.cpp文件在cpp目录下。 - 复制
OpenCv SDK的sdk -> native -> jni -> include文件到 cpp目录下。 - 新建
jniLibs文件,并复制sdk -> native -> libs到jniLibs

3.1.3 配置 CmakeLists.txt
新建 CmakeLists.txt 到 android 目录下:
cmake_minimum_required(VERSION 3.4.1)
include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)
add_library(libopencv_java4 SHARED IMPORTED)
set_target_properties(libopencv_java4 PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/libs/${ANDROID_ABI}/libopencv_java4.so)
add_library( # Sets the name of the library.
native-lib # Sets the library as a shared library.
SHARED # Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
find_library( # Sets the name of the path variable.
log-lib # Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib libopencv_java4 # Links the target library to the log library
android
# included in the NDK.
${log-lib} )
注:
include_directories与set_target_properties一定要使用${CMAKE_SOURCE_DIR}配置绝对路径,不然编译过程中会报找不到.so的问题。配置
CmakeLists是为了编译native-lib.cpp文件生成native-lib.so文件。
3.1.4 Gradle
将 opencv_plugin 下的 Gradle 文件添加以下内容
- 引入
c++_shared.so库 - 配置
CmakeLists.txt路径
android {
compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 16
### 引入 c++_shared.so 库
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_STL=c++_shared"
}
}
}
### 配置 CmakeLists.txt 路径
externalNativeBuild {
cmake {
path file('CMakeLists.txt')
}
}
}
3.1.4 检查是否配置成功
在 flutter 调用 DynamicLibrary.open("libnative-lib.so") 观察日志是否报错。
3.1.5 引入 FFI
为了方便 FFI 的操作,开始之前先 pubspec.yaml 引入 FFI 1.1.2。 这个库作用是,在 Dart 字符串和使用 UTF-8 和 UTF-16 编码的 C 字符串之间进行转换。
dependencies:
ffi: ^1.1.2
3.2 实现思路
- 在
Dart端读取图片,并转换成Uint8List并展示图片。 - 在
C++层分配内存,长度为Uint8List的长度,并深拷贝一份Uint8List - 将
Dart端创建的指针(Pointer) 对象,当做参数传入C++。 C++层先图片decode后转换为Mat结构体,调用cv::GaussianBlur()实现高斯模糊并encode成.PNG(其他格式也可以),最后将指针传回Dart端。- 将
C++传回的Uint8List转化成Dart Uint8List数据并渲染。

为什么在 Dart 读取图片?
在
Dart端读取图片是为了展示原图用,可以直接传文件路径在 C 层处理,可以减少一次拷贝。为什么要深拷贝一次?
Uint8List存在于Dart堆中,该堆是垃圾收集器,对象可能会被垃圾收集器移动。 因此,您必须将其转换为指向C堆的指针。图片压缩格式
1、
jpg格式:即为jpeg格式,是通过压缩改变画质和文件尺寸的格式。2、
png格式:png可以对图像进行无损压缩,并且压缩体积比jpg格式要小得多。3、
bmp格式:Windows中使用的标准图像格式。
3.3 源码
C++ 端的实现分为三步:
- 1、
decode图片转化为Mat对象 - 2、将
Mat对象高斯模糊处理 - 3、
encode图片为.png(其他格式也可以)
#define ATTRIBUTES extern "C" __attribute__((visibility("default"))) __attribute__((used))
// decode 图片
ATTRIBUTES Mat *opencv_decodeImage(
unsigned char *img,
int32_t *imgLengthBytes) {
Mat *src = new Mat();
std::vector<unsigned char> m;
__android_log_print(ANDROID_LOG_VERBOSE, "NATIVE",
"opencv_decodeImage() --- start imgLengthBytes:%d ",
*imgLengthBytes);
for (int32_t a = *imgLengthBytes; a >= 0; a--) m.push_back(*(img++));
*src = imdecode(m, cv::IMREAD_COLOR);
if (src->data == nullptr)
return nullptr;
if (DEBUG_NATIVE)
__android_log_print(ANDROID_LOG_VERBOSE, "NATIVE",
"opencv_decodeImage() --- len before:%d len after:%d width:%d height:%d",
*imgLengthBytes, src->step[0] * src->rows,
src->cols, src->rows);
*imgLengthBytes = src->step[0] * src->rows;
return src;
}
ATTRIBUTES
unsigned char *opencv_blur(
uint8_t *imgMat,
int32_t *imgLengthBytes,
int32_t kernelSize) {
// 1. decode 图片
Mat *src = opencv_decodeImage(imgMat, imgLengthBytes);
if (src == nullptr || src->data == nullptr)
return nullptr;
if (DEBUG_NATIVE) {
__android_log_print(ANDROID_LOG_VERBOSE, "NATIVE",
"opencv_blur() --- width:%d height:%d",
src->cols, src->rows);
__android_log_print(ANDROID_LOG_VERBOSE, "NATIVE",
"opencv_blur() --- len:%d ",
src->step[0] * src->rows);
}
// 2. 高斯模糊
GaussianBlur(*src, *src, Size(kernelSize, kernelSize), 15, 0, 4);
std::vector<uchar> buf(1); // imencode() will resize it
// Encoding with b mp : 20-40ms
// Encoding with jpg : 50-70 ms
// Encoding with png: 200-250ms
// 3. encode 图片
imencode(".png", *src, buf);
if (DEBUG_NATIVE) {
__android_log_print(ANDROID_LOG_VERBOSE, "NATIVE",
"opencv_blur() resulting image length:%d %d x %d", buf.size(),
src->cols, src->rows);
}
*imgLengthBytes = buf.size();
// the return value may be freed by GC before dart receive it??
// Sometimes in Dart, ImgProc.computeSync() receives all zeros while here buf.data() is filled correctly
// Returning a new allocated memory.
// Note: remember to free() the Pointer<> in Dart!
// 3. 返回data
return buf.data();
}
补充: GaussianBlur(*src, *src, Size(kernelSize, kernelSize), 15, 0, 4);: 这里
sigmaX、sigmaY、borderType数值写死了,最好的做法应当做参数传过来。在
C++所有函数上面加ATTRIBUTES extern "C" __attribute__((visibility("default")))。FFI库只能与C符号绑定,因此在C++中,这些符号添加extern C标记。还应该添加属性来表明符号是需要被Dart引用的,以防止链接器在优化链接时会丢弃符号。
Dart 端实现:
- 1、从
assets中读取图片转为Uint8List - 2、使用
malloc在C++中分配内存大小与上一步中Uint8List一样 - 3、用
FFI查找opencv_blur函数并调用。 - 4、处理返回结果,并释放指针。
Uint8List? uint8list;
@override
void initState() {
super.initState();
/// 读取图片,转换成 Uint8List
WidgetsBinding.instance!.addPostFrameCallback((_) async {
final bytes = await rootBundle.load('assets/image_lonely.jpeg');
uint8list = bytes.buffer.asUint8List();
setState(() {});
});
}
/// 高斯模糊
static Uint8List? blur(Uint8List list) {
/// 深拷贝图片
Pointer<Uint8> bytes = malloc.allocate<Uint8>(list.length);
for (int i = 0; i < list.length; i++) {
bytes.elementAt(i).value = list[i];
}
// 为图片长度分配内存
final imgLengthBytes = malloc.allocate<Int32>(1)..value = list.length;
// 查找 C++ 中的 opencv_blur() 函数
final DynamicLibrary _opencvLib =
Platform.isAndroid ? DynamicLibrary.open("libnative-lib.so") : DynamicLibrary.process();
final Pointer<Uint8> Function(
Pointer<Uint8> bytes, Pointer<Int32> imgLengthBytes, int kernelSize) blur =
_opencvLib
.lookup<
NativeFunction<
Pointer<Uint8> Function(Pointer<Uint8> bytes, Pointer<Int32> imgLengthBytes,
Int32 kernelSize)>>("opencv_blur")
.asFunction();
/// 调用高斯模糊
final newBytes = blur(bytes, imgLengthBytes, 15);
if (newBytes == nullptr) {
print('高斯模糊失败');
return null;
}
var newList = newBytes.asTypedList(imgLengthBytes.value);
/// 释放指针
malloc.free(bytes);
malloc.free(imgLengthBytes);
return newList;
}
四、FFI 拓展
4.1 FFI 中的指针与C++中的指针
上述高斯模糊的案例中,使用了指针的概念,让我想到以下问题:
那么 FFI 中指针的地址是否与 C++ 中指针的地址相同?
例如:C++计32位数的乘法。
ATTRIBUTES
int32_t *multiply(int32_t *a, int32_t b)
{
int32_t *mult = (int *)malloc(sizeof(int)); // 在 C 中分配内存
*mult = *a * b; // 计算乘法
__android_log_print(ANDROID_LOG_VERBOSE, "NATIVE",
"multiply() --- address:%d value:%d",
mult, *mult);
return mult;
}
Dart 声明一个 Pointer<Int32> 类型的指针,分配内存后初始化 value。
import 'package:ffi/ffi.dart';
static int multiply(int a, int b) {
// 打开动态库
final DynamicLibrary _opencvLib =
Platform.isAndroid ? DynamicLibrary.open("libnative-lib.so") : DynamicLibrary.process();
// 查找 C 函数 multiply()
final Pointer<Int32> Function(Pointer<Int32> a, int b) multiply =
_opencvLib.lookup<NativeFunction<Pointer<Int32> Function(Pointer<Int32> a, Int32 b)>>("multiply").asFunction();
// 调用 C 函数 multiply()
// malloc 是 import 'package:ffi/ffi.dart'; 中的对象
Pointer<Int32> pa = malloc.allocate<Int32>(1);// 为指针分配内存
pa.value = a;// pa.value 是指针指向的值
final result = multiply(pa, b);
final value = result.value;
print('dart --> multiply() address=${result.address} value=${result.value}');
// 注:这里省略了 C 层 free() 函数。
// 这里还需要将 result 指针传回 C 层,再 C 层调用 free() 不然会内存泄漏
// free(result)
malloc.free(result);
malloc.free(pa);
return value;
}
例如: 我们传入 a = 10, b = 100 如下结果:。
10 * 100 = 1000 返回的结果符合我们的预期。
同时我们可以观察到
C指针的地址,与Dart中指针对象指向的地址不一样。这是因为Dart与C在>不同堆栈中分配内存导致的。还需要注意,上述案例中在
C通过malloc分配了内存,如不使用还需要在C层调用free(), 在C声明的指针,只能在C层释放.同理在
Dart端也要释放指针。
4.2 FFI 接收 C++ 中的结构体
处理复杂的对象通常使用结构体,如何传递 C++ 结构体与 Dart 交互?
4.2.1 结构体定义:
例如: C++ 定义如下结构体
struct Message {
char *msg;
uint32_t phone;
};
那么对应 Dart 中需要如下定义
class Message extends Struct {
external Pointer<Utf8> msg;
@Uint32() // NativeType 类型注释
external int phone;
}
Struct子类声明中的所有字段声明必须具有int或float类型并使用表示本机类型的NativeType进行注释,或者必须是Pointer类型。
4.2.2 Dart 端接收结构体
C++ 定义如下函数:
ATTRIBUTES
Message createMessage(const char *msg, int32_t phone) {
__android_log_print(ANDROID_LOG_VERBOSE, "NATIVE",
"createMessage() ---msg:%s phone:%d",
msg, phone);
Message message = Message();
message.msg = "C++";
message.phone = 99999;
return message;
}
Dart 定义如下函数:
static Message createMessage() {
final DynamicLibrary _opencvLib =
Platform.isAndroid ? DynamicLibrary.open("libnative-lib.so") : DynamicLibrary.process();
/// 定义函数
final Message Function(Pointer<Utf8> msg, int phone) createMessage =
_opencvLib.lookup<NativeFunction<Message Function(Pointer<Utf8> msg, Uint32 phone)>>("createMessage").asFunction();
final msg = 'Dart'.toNativeUtf8(); /// 转换成 C 能识别的 Utf8 类型
final phone = 1000;
/// 调用 C 函数
final result = createMessage(msg, phone);
print('result msg = ${result.msg.toDartString()} phone = ${result.phone}');
return result;
}
在 C++ 层打印 "Dart" 字符, 在 Dart 层打印 "C++" 字符
总结
FFI提供了直接与C++的交互能力,相对于依赖JNI的方式提升数据传递的效率- 使用
FFI调用C++能做可以做到UI统一、逻辑统一,对于写具体业务的同学而言,写一套Flutter逻辑和视图双端即可运行,基本相当于客户端原生开发双倍的开发效率。在后期功能维护上,投入的成本也远远小于原生开发。 - 编写
FFI可以抽象成在C++一端开发,掌握C++基础的同学上手成本较低。 - 未来可以考虑将项目中通过
JNI方式调用.so库的业务替换成FFI提升体验,减少后期维护成本。 - 从
Flutter 2.0发布FFI稳定版,到Flutter 2.2FFI支持数组结构体, 可见Flutter成为首选的多平台开发UI工具包之势日趋明显.
参考
转载自:https://juejin.cn/post/6976824832595853342