一、需求背景
如在
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 加载动态链接库,并返回句柄。
拿到对应的动态链接库的句柄之后,就能使用相关方法进行操作了。
句柄主要包含以下两个方法:
external Pointer<T> lookup<T extends NativeType>(String symbolName);
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();
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
Open Cv 下载地址
解压后包含以下内容:
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(
native-lib
SHARED
src/main/cpp/native-lib.cpp )
find_library(
log-lib
log )
target_link_libraries(
native-lib libopencv_java4
android
${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
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_STL=c++_shared"
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))
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) {
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);
GaussianBlur(*src, *src, Size(kernelSize, kernelSize), 15, 0, 4);
std::vector<uchar> buf(1);
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();
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();
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;
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));
*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();
final Pointer<Int32> Function(Pointer<Int32> a, int b) multiply =
_opencvLib.lookup<NativeFunction<Pointer<Int32> Function(Pointer<Int32> a, Int32 b)>>("multiply").asFunction();
Pointer<Int32> pa = malloc.allocate<Int32>(1);
pa.value = a;
final result = multiply(pa, b);
final value = result.value;
print('dart --> multiply() address=${result.address} value=${result.value}');
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()
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();
final phone = 1000;
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
工具包之势日趋明显.