目录
- 需求背景
- 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.2
FFI
支持数组结构体, 可见Flutter
成为首选的多平台开发UI
工具包之势日趋明显.
参考
今天的文章如何在 Flutter 中调用 C++ 代码分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17719.html