Flutter调用SO库完全指南:从入门到精通

3 阅读10分钟

在Flutter开发中,我们经常会遇到需要调用原生代码的场景。无论是为了使用现有C/C++库,还是为了获得更好的性能,SO库(动态链接库)都是一个非常实用的解决方案。

今天我就来手把手教你如何在Flutter中使用SO动态库,从环境搭建到实际调用,让你一次就能学会!

为什么要用SO库?

在Flutter项目中,使用SO库有以下几个重要场景:

场景1:调用现有C/C++库 公司有个用了10年的图像处理库,用C++写的。总不能为了Flutter重写一遍吧?直接调用SO库是最快捷的方案。

场景2:跨平台复用核心逻辑 游戏的核心算法、加密算法这些,用C写一次,Android、iOS、Web都能用,大大减少开发和维护成本。

场景3:利用硬件加速 一些需要底层硬件支持的功能,比如音视频处理、传感器数据处理等,用C/C++可以更好地利用硬件特性。

场景4:保护核心算法 将核心算法放在SO库中,可以一定程度上保护代码不被反编译,提高安全性。

Flutter中使用SO库的两种方式

Flutter提供了两种方式来使用SO库,各有各的优缺点,根据你的需求选择合适的方式:

方式一:FFI直接调用(推荐新手)

这种方式最直接,Dart代码直接调用C函数,中间没有Kotlin/Java这一层。

优点:

  • 代码简单,学习成本低
  • 跨平台,Android、iOS、Linux都能用
  • 不需要编写额外的原生代码

缺点:

  • 只能调用C函数,不能直接用Android API
  • 复杂的系统功能(权限、通知等)不好处理

适用场景:

  • 纯计算任务(数学运算、加密算法)
  • 需要跨平台
  • 调用现有C/C++库

方式二:Android插件方式(推荐进阶)

这种方式通过MethodChannel,Dart调用Kotlin,再由Kotlin调用JNI函数。

优点:

  • 可以充分利用Android原生API
  • 适合复杂原生交互
  • 错误处理和日志更方便

缺点:

  • 多了一层Kotlin,代码量更多
  • 仅限Android平台
  • 需要维护两套代码

适用场景:

  • 需要调用Android系统API(蓝牙、相机、文件系统)
  • 需要处理权限、生命周期
  • 不需要跨平台

实战:从零开始使用SO库

我准备了一个完整的示例项目,两种方式都有,方便大家学习。

项目结构说明:

flutter-so-example/
├── lib/                    # Dart代码目录
│   ├── calculate.dart      # FFI调用封装
│   ├── main.dart           # 主界面
│   └── plugin_wrapper.dart # 插件方式调用封装
├── android/                # Android原生代码
│   └── app/
│       └── src/
│           └── main/
│               ├── jniLibs/ # SO库存放目录
│               │   ├── arm64-v8a/
│               │   ├── armeabi-v7a/
│               │   ├── x86/
│               │   └── x86_64/
│               └── kotlin/  # Kotlin代码
├── native/                 # C/C++源代码
│   ├── native_lib.c        # FFI方式C代码
│   └── plugin_lib.c        # 插件方式C代码
└── pubspec.yaml            # 项目配置文件依赖

第一步:准备C代码

先写一个简单的C函数,比如加法、减法这些基本运算:

// native_lib.h
#ifndef NATIVE_LIB_H
#define NATIVE_LIB_H

int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);

#endif
// native_lib.c
#include "native_lib.h"

int add(int a, int b) {
  return a + b;
}

int subtract(int a, int b) {
  return a - b;
}

int multiply(int a, int b) {
  return a * b;
}

int divide(int a, int b) {
  return a / b;
}

第二步:编译SO库

这里有个重要的点:Android设备CPU架构不一样,得为每个架构都编译一遍。

编译命令:

# arm64-v8a(64位ARM,大多数现代手机)
aarch64-linux-android21-clang -shared -fPIC native_lib.c -o libcalculate.so

# armeabi-v7a(32位ARM,老手机)
armv7a-linux-androideabi21-clang -shared -fPIC native_lib.c -o libcalculate.so

# x86_64(64位模拟器)
x86_64-linux-android21-clang -shared -fPIC native_lib.c -o libcalculate.so

# x86(32位模拟器)
i686-linux-android21-clang -shared -fPIC native_lib.c -o libcalculate.so

编译后放置位置: 编译出来的.so文件要放到android/app/src/main/jniLibs/对应架构目录下:

android/app/src/main/jniLibs/
├── arm64-v8a/
│   └── libcalculate.so
├── armeabi-v7a/
│   └── libcalculate.so
├── x86/
│   └── libcalculate.so
└── x86_64/
    └── libcalculate.so

编译流程说明:

  1. 准备NDK环境:确保已安装Android NDK(建议使用r21及以上版本)

  2. 选择编译器:根据目标架构选择对应的编译器

    • arm64-v8a:aarch64-linux-android21-clang
    • armeabi-v7a:armv7a-linux-androideabi21-clang
    • x86_64:x86_64-linux-android21-clang
    • x86:i686-linux-android21-clang
  3. 编译参数说明

    • -shared:生成动态链接库(.so文件)
    • -fPIC:生成位置无关代码,动态库必需
    • -o:指定输出文件名
  4. 编译命令示例

    aarch64-linux-android21-clang -shared -fPIC native_lib.c -o libcalculate.so
    
  5. 验证编译结果:使用file命令检查生成的.so文件架构

    file libcalculate.so
    # 输出示例:libcalculate.so: ELF 64-bit LSB shared object, ARM aarch64
    

第三步:FFI方式调用

在Dart中用FFI调用C函数,这是最简单直接的方式:

1. 添加依赖pubspec.yaml中添加ffi依赖:

dependencies:
  flutter:
    sdk: flutter
  ffi: ^2.1.0

2. 定义FFI绑定 创建一个calculate.dart文件:

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

// 定义函数类型
typedef AddFunc = Int32 Function(Int32 a, Int32 b);
typedef Add = int Function(int a, int b);

typedef SubtractFunc = Int32 Function(Int32 a, Int32 b);
typedef Subtract = int Function(int a, int b);

typedef MultiplyFunc = Int32 Function(Int32 a, Int32 b);
typedef Multiply = int Function(int a, int b);

typedef DivideFunc = Int32 Function(Int32 a, Int32 b);
typedef Divide = int Function(int a, int b);

class Calculate {
  late final DynamicLibrary _lib;
  late final Add _add;
  late final Subtract _subtract;
  late final Multiply _multiply;
  late final Divide _divide;

  Calculate() {
    // 加载SO库
    _lib = DynamicLibrary.open('libcalculate.so');
    
    // 查找函数并绑定
    _add = _lib.lookup<NativeFunction<AddFunc>>('add').asFunction();
    _subtract = _lib.lookup<NativeFunction<SubtractFunc>>('subtract').asFunction();
    _multiply = _lib.lookup<NativeFunction<MultiplyFunc>>('multiply').asFunction();
    _divide = _lib.lookup<NativeFunction<DivideFunc>>('divide').asFunction();
  }

  // 对外暴露方法
  int add(int a, int b) => _add(a, b);
  int subtract(int a, int b) => _subtract(a, b);
  int multiply(int a, int b) => _multiply(a, b);
  int divide(int a, int b) => _divide(a, b);
}

3. 在Flutter中调用

// 创建实例
final calculate = Calculate();

// 调用函数
final sum = calculate.add(10, 5);  // 返回15
final difference = calculate.subtract(10, 5);  // 返回5
final product = calculate.multiply(10, 5);  // 返回50
final quotient = calculate.divide(10, 5);  // 返回2

就是这么简单!Dart直接调用C函数,代码清晰易懂。

第四步:Android插件方式调用

如果需要调用Android系统API,或者需要更复杂的原生交互,可以使用Android插件方式。

1. 创建Flutter插件

flutter create --template=plugin --platforms=android load_so_plugin

2. 写JNI函数

C函数要按照JNI规范命名,创建plugin_lib.c

#include <jni.h>

// 格式:Java_包名_类名_方法名
// 包名中的点(.)替换为下划线(_)
JNIEXPORT jint JNICALL
Java_com_example_load_1so_1plugin_LoadSoPlugin_add(JNIEnv *env, jobject thiz, jint a, jint b) {
  return a + b;
}

JNIEXPORT jint JNICALL
Java_com_example_load_1so_1plugin_LoadSoPlugin_subtract(JNIEnv *env, jobject thiz, jint a, jint b) {
  return a - b;
}

3. 编译JNI库

编译成libplugin_lib.so,放到插件的android/src/main/jniLibs/目录。

4. Kotlin加载SO库

LoadSoPlugin.kt中:

class LoadSoPlugin : FlutterPlugin, MethodCallHandler {
  init {
    // 加载SO库
    System.loadLibrary("plugin_lib")
  }

  // 声明native方法
  private external fun add(a: Int, b: Int): Int
  private external fun subtract(a: Int, b: Int): Int

  override fun onMethodCall(call: MethodCall, result: Result) {
    when (call.method) {
      "add" -> {
        val a = call.argument<Int>("a")
        val b = call.argument<Int>("b")
        result.success(add(a!!, b!!))
      }
      "subtract" -> {
        val a = call.argument<Int>("a")
        val b = call.argument<Int>("b")
        result.success(subtract(a!!, b!!))
      }
      else -> result.notImplemented()
    }
  }

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

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

5. Dart通过MethodChannel调用

load_so_plugin.dart中:

class LoadSoPlugin {
  static const MethodChannel _channel = MethodChannel('load_so_plugin');

  static Future<int> add(int a, int b) async {
    final result = await _channel.invokeMethod<int>('add', {
      'a': a,
      'b': b,
    });
    return result!;
  }

  static Future<int> subtract(int a, int b) async {
    final result = await _channel.invokeMethod<int>('subtract', {
      'a': a,
      'b': b,
    });
    return result!;
  }
}

6. 在Flutter中使用

// 调用插件方法
final sum = await LoadSoPlugin.add(10, 5);
final difference = await LoadSoPlugin.subtract(10, 5);

常见问题和解决方案

问题1:找不到SO库

错误信息:

UnsatisfiedLinkError: dlopen failed: library "libcalculate.so" not found

解决方案:

  1. 确认SO库放在android/app/src/main/jniLibs/对应架构目录
  2. 检查设备架构:adb shell getprop ro.product.cpu.abi
  3. 确保编译时用的API级别和设备匹配

问题2:函数签名不匹配

错误信息:

Invalid argument(s): 'add'

解决方案: 仔细检查函数类型定义,确保Dart和C函数的参数类型一致:

// C函数:int add(int a, int b)
// Dart定义:
typedef AddFunc = Int32 Function(Int32 a, Int32 b);  // 必须匹配

问题3:JNI函数命名错误

错误信息:

java.lang.UnsatisfiedLinkError: No implementation found for int add

解决方案: 严格按照JNI规范命名函数:Java_包名_类名_方法名

  • 包名中的点(.)要替换成下划线(_)
  • 包名中的下划线(_)要替换成_1

问题4:64位和32位架构兼容

解决方案: 为所有支持的架构都编译SO库,包括:

  • arm64-v8a(64位ARM)
  • armeabi-v7a(32位ARM)
  • x86(32位模拟器)
  • x86_64(64位模拟器)

项目结构最佳实践

一个好的SO库项目结构应该是这样的:

flutter-so-example/
├── lib/
│   ├── calculate.dart       # FFI调用
│   ├── main.dart            # 主界面
│   └── performance_test.dart # 性能测试(可选)
├── android/
│   ├── app/
│   │   └── src/
│   │       └── main/
│   │           ├── jniLibs/  # SO库存放目录
│   │           └── kotlin/
│   └── build.gradle
├── plugins/
│   ├── calculate/           # FFI库
│   │   ├── native_lib.c
│   │   └── build.bat        # 编译脚本
│   └── plugin_lib/          # 插件库
│       ├── plugin_lib.c
│       └── build.bat        # 编译脚本
└── pubspec.yaml

编译脚本推荐

为了方便编译SO库,我准备了一个批处理脚本build.bat

@echo off

:: 设置NDK路径(根据你的实际安装位置修改)
set NDK_PATH=D:\Android\Sdk\ndk\25.2.9519653

:: 编译目录
set OUTPUT_DIR=..\..\android\app\src\main\jniLibs

:: 创建输出目录
mkdir %OUTPUT_DIR%\arm64-v8a 2>nul
mkdir %OUTPUT_DIR%\armeabi-v7a 2>nul
mkdir %OUTPUT_DIR%\x86 2>nul
mkdir %OUTPUT_DIR%\x86_64 2>nul

:: 编译arm64-v8a
echo 编译 arm64-v8a...
%NDK_PATH%\toolchains\llvm\prebuilt\windows-x86_64\bin\aarch64-linux-android21-clang -shared -fPIC native_lib.c -o %OUTPUT_DIR%\arm64-v8a\libcalculate.so

:: 编译armeabi-v7a
echo 编译 armeabi-v7a...
%NDK_PATH%\toolchains\llvm\prebuilt\windows-x86_64\bin\armv7a-linux-androideabi21-clang -shared -fPIC native_lib.c -o %OUTPUT_DIR%\armeabi-v7a\libcalculate.so

:: 编译x86
echo 编译 x86...
%NDK_PATH%\toolchains\llvm\prebuilt\windows-x86_64\bin\i686-linux-android21-clang -shared -fPIC native_lib.c -o %OUTPUT_DIR%\x86\libcalculate.so

:: 编译x86_64
echo 编译 x86_64...
%NDK_PATH%\toolchains\llvm\prebuilt\windows-x86_64\bin\x86_64-linux-android21-clang -shared -fPIC native_lib.c -o %OUTPUT_DIR%\x86_64\libcalculate.so

echo 编译完成!
pause

使用方法:

  1. 修改脚本中的NDK路径为你的实际安装位置
  2. 双击运行build.bat
  3. 脚本会自动编译所有架构的SO库并复制到正确位置

实际应用场景

学会了SO库的使用,你可以:

1. 集成OpenCV做图像处理

  • 图像滤镜、边缘检测
  • 人脸识别、目标跟踪
  • 图像分割、特征提取

2. 集成FFmpeg做音视频处理

  • 视频解码、编码
  • 音频处理、混音
  • 视频剪辑、特效

3. 集成TensorFlow Lite做AI推理

  • 图像分类、目标检测
  • 语音识别、自然语言处理
  • 实时AI应用

4. 集成加密算法库

  • 安全通信、数据加密
  • 哈希计算、数字签名
  • 密码学应用

总结

使用SO库是Flutter开发中的一个重要技能,它让我们能够:

  • 复用现有代码:直接使用成熟的C/C++库
  • 跨平台开发:一套代码多平台使用
  • 访问底层功能:调用系统API和硬件特性
  • 保护核心算法:提高代码安全性

选择建议:

  • 新手:从FFI开始,简单直接
  • 需要原生API:使用Android插件方式
  • 跨平台需求:优先使用FFI
  • 复杂原生交互:使用插件方式

后续学习资源


如果这篇文章对你有帮助,欢迎点赞收藏!有问题评论区见!