在上一节中我们讲了如何使用NDK编译C/C++项目。编译完成后,我们会拿到so文件以及对应的头文件。在java工程中,我们可以通过jni来调用。而在flutter中,我们可以借助dart调用C/C++代码能力调用它。

好多教程,包括官方教程都是将先包装成一个插件,然后再在flutter里面调用。我们尝试一下直接在flutter工程中调用C/C++。我们改造一下flutter的demo。默认情况下,每次点击+号按钮时,dart就是累加计数,并将计数显示出来。我们不仅仅把计数显示出来,还把数字转换成字符串后调用C++的hello函数。该函数在数字前加一个“hello ”之后返回。最后,在显示在界面上面出返回的“hello 数字”。

flowchart TB subgraph 点击按钮 _counter(累加数字) HelloNative(hello函数封装) display(刷新界面显示数据) end subgraph so库 hello(hello函数) end _counter--数字---HelloNative HelloNative-.数字.->hello hello-.hello+数字.->HelloNative HelloNative--hello+数字-->display

调整导出函数

首先,我们通过ndk已经把so库编译好了。但是,编译so库的时候需要注意,dart语言只能调用C类型的导出函数,无法调用C++导出函数。所以,头文件中的导出函数必须带上extern "C"。例如:

  • hello.h

    #ifndef HELLO_H
    #define HELLO_H
    extern "C" {
    
    const char* hello(const char* msg);
    
    }
    #endif

    其中,第3和7行是必不可少的。没有了这两行,编译出来的库无法在flutter中直接使用。

调用导出函数

创建工程

我们创建一个demo功能。

flutter create demo

引入so

  1. 将so加入到功能中。
cd demo
mkdir -p android/app/src/main/libs/arm64-v8a
cp libhello.so android/app/src/main/libs/arm64-v8a

如果需要armeabi-v7ax86或者其他硬件类型的也如法炮制即可。

  1. 配置build.gradle

文件路径:android/app/build.gradle

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
        main.jniLibs.srcDirs 'src/main/libs'
    }
    ......
    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug 
           ndk {
               abiFilters 'arm64-v8a'//,'armeabi-v7a','armeabi','x86'
           }
        }
        debug { 
           ndk {
               abiFilters 'arm64-v8a'//,'armeabi-v7a','armeabi','x86'
           }
        }
    }

其中,第3、11~19行是新增。第3行的含义是告诉编译器so库的位置,编译打包时会把这些文件打包到apk的lib目录下面。

第11~19行是告诉编译器,我们只需要arm64-v8a这一个硬件平台。如果你需要更多,可以自己调整。

封装调用

新增一个调用类,对C/C++库进行一个调用封装,以方便使用。

创建文件:lib/native_hello.dart

import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';

typedef Hello = Pointer<Utf8> Function(Pointer<Utf8> str);

class HelloNative {
  final DynamicLibrary dylib = DynamicLibrary.open("libhello.so");
  late Hello fnHello;
  HelloNative() {
    fnHello = dylib.lookupFunction<Hello, Hello>('hello');
  }

  String hello(String msg) {
    final msgUtf8 = msg.toNativeUtf8();
    final ret = fnHello(msgUtf8).toDartString();

    calloc.free(msgUtf8);

    return ret;
  }
}

其中:

  • 第1行:我们引入了ffi这个包,它提供了dart调用C/C++的能力。

  • 第3行:dart内置的ffi包中没有Utf8类型,为了处理字符串,我们还没有引入ffi/ffi.dart'

  • 第5行:定义了一个函数类型。跟C/C++中的比较相像,不同的是数据类型。ffi对两种语言的各种数据类型都做映射封装,具体如下:

    C/C++类型 dart类型 说明
    bool Bool
    double Double
    float Float
    int8 Int8
    int16 Int16
    int32 Int32
    int64 Int64
    函数指针 NativeFunction 函数指针类型。
    不过,需要注意的是在dart中无法定义一个函数指针类型。
    这也就导致了无法在C/C++中调用dart的函数。
    ?? Opaque 不清楚在C/C++中是啥。官方文档中也是语焉不详。
    uint8 Uint8
    uint16 Uint16
    uint32 Uint32
    uint64 Uint64
    void Void
    指针 Pointer 指针类型。Pointer是个模板类
    Pointer<Int8>指的就是int8 *,其他类型也一样。
    数组 Array 数组类型。跟Pointer一样,Array也是个模板类
    Array<Int8>(10)就是int8 xxx[10]
    Array<Int8>(10, 20)就是int8 xxx[10][20]
    ……

    对于字符串类型,这比较特别。在C/C++中,字符串就是以\0结尾的内存块。但是在dart中,字符串是个类。所以,我们需要使用String类型的toNativeUtf8方法得到这个内存块。然后,借助ffi的模板类Pointer对类型进行转换包装。不过,需要留意的是toNativeUtf8方法返回的内存块是需要手动释放的。

    另外,除了基本类型还对结构体、联合体和数组进行了类型转换封装。关于这三种,我们后面讨论。

  1. 第8行,是加载动态库。需要注意的是,我们不需要在这儿判断硬件类型。我们前面已经把不同类型的so放在了不同的目录里面了。dart运行时会处理这部分逻辑。
  2. 第11行,就是从so库中找到对应的导出函数,并获得其函数指针。其实,lookupFunction这个模板函数是支持两个函数类型的,一个是C函数类型,另一个是dart函数类型。
  3. 第14~21行则是实际调用方法。

最后,改一下MyHomePage,调用函数hello,并将返回值显示在界面上,代码如下:

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  final _hello = HelloNative();
  String msg = "";

  void _incrementCounter() {
    setState(() {
      _counter++;
      msg = _hello.hello(_counter.toString());
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(msg),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

运行效果

需要注意的是因为例子仅仅支持了arm64-v8a这种硬件,所以,无法在x86的模拟器中运行。如果你运行失败,最好是在真机中试一下。

本文所有代码均可在ndk-test获得。