系列文章

在《2.flutter调用C/C++》中我们提到,flutter可以通过ffi库直接调用C/C++的函数。但是,很可惜,无法在C/C++中回调flutter的函数。

本文就来探讨一下如何在C/C++中调用flutter的函数。中间需要有一个桥梁,那就是flutter的插件。在flutter插件中可以实现原生代码(java、kotlin等)与flutter的相互调用。而这些语言又可以与C/C++实现互调。

graph TB subgraph flutter App app("dart") end subgraph flutter插件 plugin("dart") native("java、kotlin") c("C/C++") end app-->plugin plugin-->app plugin-->native native-->plugin native-->c c-->native

创建插件工程

可以通过下面的命令创建一个插件工程:

flutter create -t plugin --org top.mydata --platforms android demo_plugin_with_ndk

其中,

  • -t指定创建的是一个插件,
  • --org参数指定组织,
  • --platforms参数指定平台类型。
  • 最后一个参数是插件的名称。

注:如果后面需要多个平台,多次执行该命令,同时--platforms使用不同的参数。例如:

flutter create -t plugin --org top.mydata --platforms ios demo_plugin_with_ndk
flutter create -t plugin --org top.mydata --platforms macos demo_plugin_with_ndk
flutter create -t plugin --org top.mydata --platforms linux demo_plugin_with_ndk
flutter create -t plugin --org top.mydata --platforms windows demo_plugin_with_ndk
flutter create -t plugin --org top.mydata --platforms web demo_plugin_with_ndk

C/C++代码

源码目录

创建目录android/src/main/cpp,并在该目录中创建两个文件:

  • CMakeLists.txt

    cmake_minimum_required(VERSION 3.22.1)
    
    set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
    
    project("demo_hello")
    
    add_library(demo_hello SHARED hello.cpp)
  • hello.cpp

    #include <jni.h>
    #include <string>
    
    extern "C" JNIEXPORT void JNICALL
    Java_top_mydata_demo_1plugin_1with_1ndk_DemoPluginWithNdkPlugin_hello(
        JNIEnv *env,
        jobject thiz,
        jstring m1)
    {
        jclass cls = env->FindClass("top/mydata/demo_plugin_with_ndk/DemoPluginWithNdkPlugin");
        jmethodID jmid = env->GetMethodID(cls, "onShowMsg", "(Ljava/lang/String;)V");
        if (!jmid)
        {
            return;
        }
        std::string strM1 = env->GetStringUTFChars(m1, 0);
        strM1 += "\nhello in c++(Java_top_mydata_demo_1plugin_1with_1ndk_DemoPluginWithNdkPlugin_hello)";
        env->CallObjectMethod(thiz,
                              jmid,
                              env->NewStringUTF(strM1.c_str()));
    }

这个jni函数的实现非常简单,调用类DemoPluginWithNdkPluginonShowMsg函数。具体的规则可以查看jni的相关资料。

库文件

创建目录android/src/main/libs,并在该目录中创建两个文件:

libs
├── arm64-v8a
│   └── libc++_shared.so
└── armeabi-v7a
    └── libc++_shared.so

需要留意的是在flutter中必须要带上libc++_shared.so,不然,会导致so库加载失败。

另外,需要留意的是,如果有so在CMake中导入了,那个就不能放在这个目录中。而需要放在另外的目录里面。否则,编译时会报so重复错误。比如CMake里面:

... ...
add_library( libcrypto
              SHARED
              IMPORTED  )
set_target_properties(libcrypto
                PROPERTIES IMPORTED_LOCATION
                ${CMAKE_SOURCE_DIR}/../libs2/${ANDROID_ABI}/libcrypto.so)
... ...

这样编译时就会自动输出libcrypto.so。这个so就不应该放在libs,而需要创建一个新的目录,如libs2

配置build.gradle

android/build.gradle中新增以下内容:

... ...
   sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
        // 指定C++文件的目录
        main.jni.srcDirs += 'src/main/cpp'
        // 指定库文件所在目录,这儿的库文件,不参与编译。但是,会被打包到最终的apk中。
        main.jniLibs.srcDirs 'src/main/libs'
    }

    defaultConfig {
        minSdkVersion 16
        // 指定ndk的版本号。
        ndkVersion "25.1.8937393"
        // 指定需要编译哪些CPU指令集。
        ndk {
            abiFilters "arm64-v8a", "armeabi-v7a"
        }
    }

    // 指定cmake配置文件的位置和版本号。
    externalNativeBuild {
        cmake {
            path file('src/main/cpp/CMakeLists.txt')
            version '3.22.1'
        }
    }
... ...

Kotlin代码

package top.mydata.demo_plugin_with_ndk

import android.app.Activity
import androidx.annotation.NonNull
import android.util.Log

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.io.IOException
import java.lang.Exception
import java.util.*

/** DemoPluginWithNdkPlugin */
class DemoPluginWithNdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private lateinit var channel : MethodChannel
  private lateinit var activity: Activity

  // 在构造函数中加载so
  init {
    try {
      System.loadLibrary("demo_hello")
    }
    catch(e: Exception){
      Log.d("TAG", "load demo_hello failed! ${e.localizedMessage}")
    }
  }

  // { 这4个函数是继承自ActivityAware,是为了支持多线程。其实,在本例中并非必需。
  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activity = binding.activity
  }

  override fun onDetachedFromActivity() {
    // no op
  }

  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    activity = binding.activity
  }

  override fun onDetachedFromActivityForConfigChanges() {
    // no op
  }
  // }

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "demo_plugin_with_ndk")
    channel.setMethodCallHandler(this)
  }

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    // 根据函数并调用对应的处理函数
    when(call.method){
      "hello" -> handleHello(call, result)
    }
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }

  fun handleHello(@NonNull call: MethodCall, @NonNull result: Result){
    // 获取函数参数。
    val m1 = call.argument<String>("m1")!!

    // 调用jni函数。
    hello(m1 + "\nhello in kotlin(DemoPluginWithNdkPlugin::handleHello)");

    // 返回成功
    result.success(null)
  }

  external fun hello(@NonNull m1: String)

  // C++中回调的函数
  fun onShowMsg(msg: String){
    // 为了支持多线程,我们需要切换到UI线程再回调
    activity.runOnUiThread {
      channel.invokeMethod("onShowMsg",
        mapOf("msg" to msg))
    }
  }
}

注意:

  • 需要继承ActivityAware,这是因为,回调的时候可能在工作线程中,只能在UI线程中操作UI,所以,我们需要切换到UI线程再回调。不过,在本例子中,C++中并没有使用线程,所以,这些并不是必须的。

  • 在调用jni前,需要使用System.loadLibrary("demo_hello")加载so库。通常,调用一次就可以了。

Flutter插件代码

这个部分的代码逻辑在《编写flutter插件》一文中有详细的解说,完全相同,不再重复讲述,代码如下:

lib/demo_plugin_with_ndk.dart:

import 'demo_plugin_with_ndk_platform_interface.dart';

class DemoPluginWithNdk {
  // 启动函数,目录是注册一个回调函数。
  Future<void> start({required DemoShowMsg onShowMsg}) {
    return DemoPluginWithNdkPlatform.instance.start(onShowMsg);
  }

  // 功能函数,在函数中最终会回调会App的代码。
  Future<void> hello(String m1) {
    return DemoPluginWithNdkPlatform.instance.hello("$m1\nhello in dart(DemoPluginWithNdk::hello)");
  }
}

lib/demo_plugin_with_ndk_method_channel.dart:

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'demo_plugin_with_ndk_platform_interface.dart';

/// An implementation of [DemoPluginWithNdkPlatform] that uses method channels.
class MethodChannelDemoPluginWithNdk extends DemoPluginWithNdkPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('demo_plugin_with_ndk');
  DemoShowMsg? _onShowMsg;
  MethodChannelDemoPluginWithNdk() {
    methodChannel.setMethodCallHandler(_handleMethodCall);
  }

  // 处理Kotlin调用过来的函数调用请求。
  Future<void> _handleMethodCall(MethodCall call) async {
    switch (call.method) {
      case 'onShowMsg':
        _handleOnShowMsg(call);
        break;
      default:
        throw ('Not implemented: ${call.method}');
    }
  }

  void _handleOnShowMsg(MethodCall call) async {
    final msg = call.arguments["msg"];
    await _onShowMsg?.call(msg+"\nhello in dart(_handleOnShowMsg)");
  }

  @override
  Future<void> start(DemoShowMsg onShowMsg) async {
    _onShowMsg = onShowMsg;
  }

  @override
  Future<void> hello(String m1) async {
    methodChannel.invokeMethod<void>('hello', {
      "m1": m1
    });
  }
}

lib/demo_plugin_with_ndk_platform_interface.dart:

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'demo_plugin_with_ndk_method_channel.dart';

typedef DemoShowMsg = Future<void> Function(String msg);

abstract class DemoPluginWithNdkPlatform extends PlatformInterface {
  /// Constructs a DemoPluginWithNdkPlatform.
  DemoPluginWithNdkPlatform() : super(token: _token);

  static final Object _token = Object();

  static DemoPluginWithNdkPlatform _instance = MethodChannelDemoPluginWithNdk();

  /// The default instance of [DemoPluginWithNdkPlatform] to use.
  ///
  /// Defaults to [MethodChannelDemoPluginWithNdk].
  static DemoPluginWithNdkPlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [DemoPluginWithNdkPlatform] when
  /// they register themselves.
  static set instance(DemoPluginWithNdkPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<void> start(DemoShowMsg onShowMsg) {
    throw UnimplementedError('start() has not been implemented.');
  }

  Future<void> hello(String m1) {
    throw UnimplementedError('hello() has not been implemented.');
  }
}

App代码

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:demo_plugin_with_ndk/demo_plugin_with_ndk.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _message = "";
  final _demoPluginWithNdkPlugin = DemoPluginWithNdk();

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // 回调函数
  Future<void> _onShowMsg(String msg) async {
    setState(() {
      _message = "$msg\nhello in dart(_onShowMsg)";
    });
  }

  Future<void> initPlatformState() async {
    try {
      // 注册回调函数。
      await _demoPluginWithNdkPlugin.start(onShowMsg: _onShowMsg);
      // 调用功能函数。
      await _demoPluginWithNdkPlugin.hello("hello in dart(initPlatformState)");
    } on PlatformException {
      _message = 'Failed in platform.';
    }

    if (!mounted) return;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Text(_message),
        ),
      ),
    );
  }
}

最终效果

系列文章