最近琢磨着要给自己的 APP 接一个日志收集的 SDK 备用。考虑到一个问题,目前大多数开源的日志库,例如美团的 Logan
和腾讯的 XLog
,日志的存取都选择了使用 mmap
建立内存文件映射来提升读写效率和日志防丢。如果直接封装 plugin 调用 Android、iOS平台代码的话,就会出现 Flutter -> Platform -> Native 的情况。很显然,这种调用是没有必要的。那可以直接 Dart 调用 C/C 吗?答案是可以的。
实践了一下 Flutter 通过 ffi
包调用 native C/C 代码,ffi 代表 Foreign function interface
(外部函数接口),入门实践 可以在 Flutter
的官方文档(https://flutter.cn/docs/development/platform-integration/c-interop)中找到。
我们使用 DynamicLibrary
来加载 C/C 编写的动态库。在 iOS 中,可以直接在源代码目录写,在Android 中则需要在 Gradle
中配置 CMakeList
。
接下来我们以接入 Logan
的 C 代码为例来实践一下,关于 Logan
,可以参考它的 github (https://github.com/Meituan-Dianping/Logan)。
基于双端不一致的考虑,我们把 C 代码放在plugin 工程中的 ios/Classes/logan
里面, 在 Android的 cmakelist 里面,会声明这个路径:
project(clogger)
add_library( logan
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
../ios/Classes/cloger_logan.c)
set(EXTERN_DIR ../ios/Classes/logan)
# 下面部分是动态链接 cloagn 本身的代码:
add_subdirectory(${EXTERN_DIR} clogan.out)
include_directories(${EXTERN_DIR} logg)
link_directories(clogan.out)
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(logan ${log-lib} z clogan)
而C 的最终实现则放在 lib/clogan
下面。
关于C代码的编写和Cmakelist的构建,建议使用 Clion 这个IDE,非常的好用
接着在 Dart
端,可以加载我们的动态库:
在 Android 中最终是以 so 库的形式来动态链接的。所以加载native 功能的实现需要区分一下平台:
static final DynamicLibrary nativeLogLib = Platform.isAndroid
? DynamicLibrary.open("liblogan.so")
: DynamicLibrary.process();
这里如果能正常编译运行通过,基本就没有什么问题。说明 C/C 编写的库也能正常加载到。我们继续实践一下:
CLogan
在读写方面最终在 C 层暴露了下面几个函数,在 clogan_core.h
里面:
int
clogan_init(const char *cache_dirs, const char *path_dirs, int max_file, const char *encrypt_key16,
const char *encrypt_iv16);
int clogan_open(const char *pathname);
int
clogan_write(int flag, char *log, long long local_time, char *thread_name, long long thread_id,
int is_main);
int clogan_flush(void);
可以看到这里分别对应了 mmap
的建立,写入和配置。我们在 Dart 层来做一份对应的实现。
先介绍一下 dart 是如何实现对应的 c函数调用的, DynamicLibrary
中提供了 lookup
方法来查找原生类型符号并返回它在内存中的地址。我们先看一个简单的示例,2个int类型相加:
这里最后会把 lookup
的结果转换成一个 Function,通过 Function的执行,来调用C里面的逻辑得到最终结果。注意这里 Function 里面定义的类型是:
NativeFunction<Int32 Function(Int32, Int32)
这里的 NativeFunction
和 Int32
是什么呢?我们进 ffi
的源码可以看到:
原来 ffi
里面定义了 NativeType
来表示 C/C native 层的类型。看一下它的继承结构:
这里提供的全部都是基础类型。指针和结构体在 Dart
层也有封装:
class Pointer<T extends NativeType> extends NativeType {
external int get address;
}
abstract class Struct extends NativeType {
final Pointer<Struct> _addressOf;
Struct() : _addressOf = nullptr;
Struct._fromPointer(this._addressOf);
}
// 这个文件里面同时也定义了 sizeof 这个方法,对应C的sizeOf
external int sizeOf<T extends NativeType>();
回到 Logan
的调用我们就会发现,int类型参数好指定 ,String 类型则不是很好指定了,如果我们直接传 uin8的point类型,需要解决2个问题:
- Pointer 的内存分配,毕竟到了C层,它是个指针
- String如何转成 Point
官方实现了一个包来替代。但是直接在
pubspec
引入会报错,所以我直接 copy 了他的代码。 封装一个Utf8
来表示String
:
class Utf8 extends Struct {
}
把Dart的字符串转成指针:
代码语言:javascript复制static Pointer<Utf8> toUtf8(String string) {
final units = utf8.encode(string);
final Pointer<Uint8> result = allocate<Uint8>(count: units.length 1);
final Uint8List nativeString = result.asTypedList(units.length 1);
nativeString.setAll(0, units);
nativeString[units.length] = 0;
return result.cast();
}
我们分析一下这段代码的实现思路
- 先把字符串encode成 uint8的数组
- 根据数据长度来分配指针的内存大小,需要分配 length 1,因为c的字符串必须是 结尾
- 把指针转成对应dart类型的list,然后全部赋值为0
- 把char* 重新cast成
Pointer<Utf8>
这里其实就是通常c代码实现放在了 Dart 层来控制。对应的C伪代码:
char *str;
str = molloac(len 1);
memset(str, 0, len 1);
allocate
里面其实也是调用了 C 的 molloac
函数:
在Dart的调用中,我们声明 Function的类型:
代码语言:javascript复制typedef WriteLogDart = void Function(int,Pointer<Utf8>,int,Pointer<Utf8>,int,int);
typedef WriteLog = Void Function(Int32,Pointer<Utf8>,Int64,Pointer<Utf8>,Int64,Int64);
实现 write
方法:
static void write(int flag,String log,int time,String threadName,int threadId,bool isMainThread) {
final writeLogan = nativeLogLib.lookup<NativeFunction<WriteLog>>('writeLog');
final write = writeLogan.asFunction<WriteLogDart>();
write(flag, Utf8.toUtf8(log), time, Utf8.toUtf8(threadName),
threadId, isMainThread?0:1);
}
我们在调用的时候,例如 String log
,也需要先转成 Utf8
在使用,否则语法并不能检测出来 String
和 Pointer<Uint8>
其实到了C层是一个东西。
相比于 Android有封装好的 JNI, ffi
相对来说还是比较麻烦的。需要自己提供内存分配和类型转换的实现。
总结
到这里 ffi
的实践就介绍完了。示例的代码我放在了自己的 github(https://github.com/shaomaicheng/clogger) , 需要阅读的朋友可以自己去clone下来。