• Flutter 直接调用so动态库,或调用C/C++源文件内函数


    开发环境

    MacBook Pro Apple M2 Pro | macOS Sonoma 14.0
    Android Studio Giraffe | 2022.3.1 Patch 1
    XCode Version 15.0
    Flutter 3.13.2 • channel stable
    Tools • Dart 3.1.0 • DevTools 2.25.0


    先说下历程,因为我已经使用了Flutter3+的版本,起初了解到Flutter调用C/C++可以使用dart原生的 ‘dart:ffi’ 库,于是按照找到的一些文档使用,结果无论如何都会报错:“Failed to load dynamic library”,也就是在 DynamicLibrary.open('libxxx.so') 的阶段就失败了,我把 .so 文件尝试放过好几个目录 /assets/libs//lib//android/app/src/main/jniLibs/,最终都会报这个错误,现在才知道库文件需要放在你根本想象不到的地方!!!

    尝试了一些方法还是不行后我放弃了 ffi,想着用 Flutter 的 MethodChannel 桥接 android/ios 原生,再让原生去调 native 层。一顿操作把Android端的搞定了,当然中间涉及到 Kotlin和C++ 层的数据类型映射的痛苦,而且业务函数也不像一个 helloworld 那么简单,别提有多痛苦了。当我再转身去搞iOS端的时候看到了iOS应该使用静态库 .a/.framework 格式的消息,如果使用动态库上架App Store是会被驳回的!native端不是我写的,我只有动态库,虽然之前写过双端的密钥插件有点经验,但是iOS毕竟咱过于陌生需要遍地找文档,过程实在过于痛苦,于是非常不甘心地又尝试起了 ffi 的方案。

    在反复尝试了各种文档和博客后,我新建了一个Flutter C++的插件项目终于跑通了,并且找到了生成的库文件存放的位置。其实用法很简单,只是走错了路让我痛苦了一遍又一遍!!!
    Flutter 和 Dart 的 官方文档 都只讲了如何调用 C/C++函数,没有提到如何直接调用 动态库或静态库。

    中间我尝试了用Android原生 jni 调so库、用xcode写C++代码生成静态库给iOS调用、cmake交叉编译各平台库…因为 jni 的方式需要 包名、类名、入参类型、返回值类型 完全对应才能正确映射,jni 几百年不用一次又去捡了一遍这些细节知识,我真的椒麻了!!!

    下面分别示例了 Flutter直接调用so动态库Flutter调用C/C++源文件内函数,第一种方式更为简单,因为省去了编译相关的配置


    Flutter直接调用so动态库

    先讲一下流程:

    1. 在正确的地方放置 .so 文件
    2. 编写 dart 代码调用 native 函数
    3. 在 dart 代码中调用 dart 映射的函数

    案例:

    我创建了一个 C++ 项目,使用一个 .cpp 文件写了一个 add 函数,add 函数有 两个 int 类型的入参,和一个 int 类型的出参,最终打包了 libnative_add.so 文件给 Flutter项目的 Android和iOS两端使用。

    cpp 代码如下:

    #include 
    
    // extern "C" 是移动端调用 C++ 代码需要的,调用 C 代码则不需要
    extern "C" {
        int32_t add(int32_t a, int32_t b) {
            return a + b;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    映射成 dart 代码也就是:

    
    int add(int a, int b) {
      return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4

    一、.so文件放在哪个目录

    这可以说是坑了我的最关键的一步,动态库应该放的目录是:

    项目名/build/app/intermediates/merged_native_libs/debug/out/lib/,因为我是新建的插件项目,所以我的目录层级会多一级。

    .so文件目录

    对应的架构和原生一样分别放在自己的目录,比如 armeabi-v7a、arm64-v8a、x86_64,这里面也涉及真机和模拟器的一些问题

    build 目录可能在 Android Studio 内看不到,到文件夹下去操作就行,release 阶段也需要在对应目录放置库文件。是不是非常意想不到,毕竟这个目录一旦 clean 就没了,不过可以放在其他目录写个 gradle 脚本拷贝过去

    顺便提一下 Flutter 很多文件的操作都需要在“非正常目录”去操作,比如说用xcode编写iOS端插件需要在 /ios/.symlinks/... 目录去找项目,有时候 Android Studio 还会抽疯,无法直接在 Flutter 项目里 “从 Android Studio 打开 android 目录” 或者 “从 XCode 打开 iOS 目录”

    二、加载.so文件并创建映射函数

    这一步是在写dart代码的 项目名/lib/ 目录,最好额外创建一个文件去处理这块儿内容,比如文件:native.dart

    点解释了: 关于这两行代码的含义

    点详细讲解了: ffi 调用native函数的两种方式和泛型参数的两种写法

    final addLib = Platform.isAndroid ? DynamicLibrary.open('libnative_add.so') : DynamicLibrary.process();
    
    final addFunc = addLib.lookupFunction<Int32 Function(Int32, Int32), int Function(int, int)>('add');
    
    
    • 1
    • 2
    • 3
    • 4

    三、调用函数

    导入文件,直接调用就行

    import '../native.dart';
    
    int result = addFunc(1, 2);
    
    • 1
    • 2
    • 3

    到这里代码就结束了,很简单对吧,我搞了一天!!!


    四、解释一下这两行代码的含义

    第一行是加载动态库文件。Android和iOS的方式不一样,包括Mac或者Windows上也需要不同的代码兼容(这里只包含Android iOS的),动态库的原文件名就是 “libnative_add.so”,我没尝试过类似Java加载动态库的这种写法能不能行 “native_add”

    第二行是调用函数 add 。泛型部分后面有详细讲解。


    五、ffi 调用native函数的两种方式和泛型参数的两种写法

    ffi 调用native函数的两种方式

    1. ffi 调用native函数方式一
    final int Function(int x, int y) addFunc = addLib
        .lookup<NativeFunction<Int32 Function(Int32, Int32)>>('add').asFunction();
    
    • 1
    • 2
    1. ffi 调用native函数方式二
    final addFunc = addLib
        .lookupFunction<Int32 Function(Int32, Int32), int Function(int, int>('add');
    
    • 1
    • 2

    其中关键函数有两个,方式一是 lookup().asFunction(),方式二是 lookupFunction()。函数的出参和入参都是 Function 的形式。

    官方使用的方式一,这个应该是版本原因,方式二的函数估计是后面版本新增的扩展方法,反正我是用的这个函数。这几个函数的原定义如下:

    // 方式一的函数,需要配合 asFunction 函数使用
    external Pointer<T> lookup<T extends NativeType>(String symbolName);
    
    extension NativeFunctionPointer<NF extends Function> on Pointer<NativeFunction<NF>> {
    	external DF asFunction<('NF') DF extends Function>({bool isLeaf = false});
    }
    
    
    // 方式二的函数
    external F lookupFunction<T extends Function, F extends Function>(String symbolName, {bool isLeaf = false});
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    再解释一下这两个函数的泛型部分

    方式一的函数,泛型只有一个参数,传入的是 native层函数写法的出参和入参类型,且需要用 NativeFunction 类包裹,变量接收的是 dart层函数写法的出入参类型。涉及到 dart 和 C/C++ 的数据类型对照看这里

    方式二的函数,泛型有两个参数,泛型参数一是 native层函数写法的出入参类型,泛型参数二是 dart层函数写法的出入参类型

    是不是感觉有点绕,多看几眼就适应了。

    至于泛型参数部分还有其他写法,就是将泛型参数用 typedef 关键字定义出去。举个完整的栗子来清晰一下两种写法:

    1. 最原始的写法
    final lib = Platform.isAndroid ? DynamicLibrary.open('libnative_add.so') : DynamicLibrary.process();
    
    // 方式一调用
    final int Function(int x, int y) addFunc = addLib
        .lookup>('add').asFunction();
    
    // 方式二调用
    final addFunc = addLib.lookupFunction('add');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 视觉优化后的写法
    typedef AddNative = Int32 Function(Int32, Int32); //定义native层函数写法的出入参类型
    typedef AddDart = int Function(int, int); //dart层函数写法的出入参类型
    
    final lib = Platform.isAndroid ? DynamicLibrary.open('libnative_add.so') : DynamicLibrary.process();
    
    // 方式一调用
    final addDart addFunc = addLib
        .lookup>('add').asFunction();
    
    // 方式二调用
    final addFunc = addLib.lookupFunction('add');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    现在是不是理解我为什么会选用方式二了,因为语法层面会更清晰明了一些!




    Flutter调用C/C++源文件内函数

    依旧是使用官方的 ‘dart:ffi’ 调用。

    很简单,全程在 android 目录下操作,首先讲一下流程:

    1. 创建 ‘xx.cpp’ 文件,编写函数代码
    2. 创建 ‘CMakeLists.txt’ 文件,编写打包 动态库或静态库 代码
    3. 配置 cmake 编译环境和平台
    4. 使用 ffi 调用 native 函数

    一、编写 C/C++ 代码

    这里只演示了 C++,仅仅是 C 的话自行修改,注:C++ 可以不需要头文件

    #include 
    
    extern "C" {
        int32_t add(int32_t a, int32_t b) {
            return a + b;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    二、编写 CMakeLists.txt 代码

    项目名/android/ 创建文件 CMakeLists.txt
    以下文件打出来的包在如下位置,如有插件会多一层目录:
    项目名/build/app/intermediates/merged_native_libs/debug/out/lib/

    cmake_minimum_required(VERSION 3.4.1)  # 版本根据自己的需要进行修改
    
    add_library(
            # 编译打包出来的lib文件名称,以下打包出来为:libnative_add.so
            native_add
    
            # 动态库使用:SHARED、静态库使用:STATIC
            SHARED
    
            # 源文件可以放在别的地方
            native_add.cpp 
    )
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    三、配置 cmake 编译环境和平台

    打开 build.gradle,在 android 节点下配置:

    android {
        externalNativeBuild {
            cmake {
                path "CMakeLists.txt"
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    四、使用 ffi 调用 native 函数

    参照上面调用 .so 的代码

    // ffi 加载动态库,并映射 native 函数
    final addLib = Platform.isAndroid ? DynamicLibrary.open('libnative_add.so') : DynamicLibrary.process();
    
    final addFunc = addLib.lookupFunction<Int32 Function(Int32, Int32), int Function(int, int)>('add');
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    // 导入文件,调用函数
    import '../native.dart';
    
    int result = addFunc(1, 2);
    
    • 1
    • 2
    • 3
    • 4

    文末 !!!!!!!!!!加粗吐槽跨平台针对原生兼容的坑,官方文档有重要遗漏且不清晰、官方没有足够完整的案例、对于平常只接触单一平台的人很不友好!!!!!!!!!

  • 相关阅读:
    骨传导耳机推荐:2022年好用的骨传导耳机
    Win系统使用WSL子系统Linux启动vGPU增强图形性能加速OpenGL
    MobileNetV2架构解析
    Apache Pulsar 社区年度峰会 Pulsar Summit Asia 2022 即将召开
    python 并发请求,转发
    2020 MIT6.s081 Lab: xv6 lazy page allocation
    网络工程师 ---- 常见的查看命令
    门面模式简介
    腾讯Q币充值大面积取消97折优惠;马斯克抨击苹果抽成|极客头条
    Hbase大批量数据迁移之BulkLoad
  • 原文地址:https://blog.csdn.net/qq_39420519/article/details/133750099