Flutter+Rust Android, IOS移动端适配通用流程及依赖库处理(Openssl, Curl等)
文章目录
- 引言
- 1 一般流程
- 1.1 基础环境配置(以windows环境为例)
- 1.1.1 Rust环境
- 1.1.2 Rust项目配置
- 1.1.3 Android额外配置
- 1.2 Android编译.so动态文件
- 1.3 IOS编译.a静态文件
- 1.4 可执行文件链接到Flutter项目
- 1.4.1 Android配置
- 1.4.2 IOS配置
- 1.5 运行和调用
- 1.5.1 FFI方式
- 2 依赖库处理
- 2.1 Openssl库的处理
- 2.1.1 Android
- 2.1.2 IOS
- 3 编译文件压缩
引言
由于工作需求,需要在基于Flutter的移动端项目中使用到Rust实现的方法,整个适配过程比较曲折,而且由于Rust项目中有依赖Openssl等c库,涉及到一些报错,解决时走了很多弯路,所以记录下来希望能对大家起到帮助。
本文首先梳理整个适配流程,从环境安装到项目配置、运行调用等,然后介绍我在实际操作中遇到的一些问题。
- 如果需要引入的Rust项目比较简单,则阅读第一部分(一般流程)后基本就可以完成正常调用了;
- 如果像我一样,Rust项目中涉及到Openssl或者其他依赖库,编译不成功或者调用不成功,在第二部分中我记录了遇到的报错情况和解决方法可以参考。
1 一般流程
在Flutter项目中调用rust的代码,简要概括整个逻辑,就是要把rust实现的那些函数方法第一步先编译成对应架构对应平台的可执行文件,第二步按照对应平台的链接方式,把可执行文件链接到Flutter项目中去,第三步使用ffi方式去完成调用,调用可执行文件中的某个方法。如:
- Android平台需要把rust编译成支持arm64-v8a、x86_64、armabi-v7a架构的.so动态文件;
- IOS平台需要把rust编译成对应arm64或者x86架构的.a静态文件,或者同时支持两种架构的.a文件,以支持在真机或者在模拟器上的导入和使用。
这部分会详细介绍这几个步骤。
1.1 基础环境配置(以windows环境为例)
1.1.1 Rust环境
下载地址: https://rustup.rs/ ,windows系统点击rustup-init.exe,mac系统按照提示安装。
注意:这一步如果没安装过Visual Studio需要安装。
安装好后,打开命令行输入cargo --version检查是否安装成功:
如果提示”不是内部或外部命令,也不是可运行程序“,请检查安装,检查环境变量的Path中是否存在cargo/bin,如下:
1.1.2 Rust项目配置
在Cargo.toml中添加如下配置:
[lib]
crate-type = ["staticlib", "cdylib"]
[build-dependencies]
flutter_rust_bridge_codegen = "=1.82.6"
- crate-type:表示编译出的文件格式,staticlib为静态文件,cdylib为动态文件,一般前者用于ios,后者用于安卓。
- flutter_rust_bridge_codegen:用于自动生成ffi文件。
1.1.3 Android额外配置
一、安装LLVM
- 运行命令:
winget install -e --id LLVM.LLVM
二、Android ndk安装
- 通过Android Studio的SDK Manager安装NDK 23.1.7779620 , 注意NDK必须为23版本,否则会报错:
- 安装cargo ndk:
运行命令:cargo install cargo-ndk
1.2 Android编译.so动态文件
Android端做完以上准备以后,来到重点:将你的rust项目编译成适用于android三种架构的.so结尾的动态库,以供app调用。
-
添加cargo的android编译工具:
依次在命令行输入以下命令rustup target add aarch64-linux-android rustup target add armv7-linux-androideabi rustup target add x86_64-linux-android rustup target add i686-linux-android
-
配置gradle.properties和local.properties:
在 android/gradle.properties 中将ANDROID_NDK路径改为本地安装的路径
在 android/local.properties 中加入NDK路径:
build.gradle添加配置(参考rust官方文档:https://trdthg.github.io/flutter_rust_bindgen_book_zh/)[ new Tuple2('Debug', ''), new Tuple2('Profile', '--release'), new Tuple2('Release', '--release') ].each { def taskPostfix = it.first def profileMode = it.second tasks.whenTaskAdded { task -> if (task.name == "javaPreCompile$taskPostfix") { task.dependsOn "cargoBuild$taskPostfix" } } tasks.register("cargoBuild$taskPostfix", Exec) { // Until https://github.com/bbqsrc/cargo-ndk/pull/13 is merged, // this workaround is necessary. def ndk_command = """cargo ndk \ -t armeabi-v7a -t arm64-v8a -t x86_64 \ -o ../android/app/src/main/jniLibs build $profileMode""" workingDir "../../rust" environment "ANDROID_NDK_HOME", "$ANDROID_NDK" if (org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem.isWindows()) { commandLine 'cmd', '/C', ndk_command } else { commandLine 'sh', '-c', ndk_command } } }
添加完以后,运行app,运行过程中即会进行编译的步骤,将rust代码编译为.so的动态文件,在rust文件夹内对应的输出部分,或者在刚刚定义的:
def ndk_command = """cargo ndk \
-t armeabi-v7a -t arm64-v8a -t x86_64 \
-o ../android/app/src/main/jniLibs build $profileMode"""
即android/app/src/main/jinLibs中能够找到。
1.3 IOS编译.a静态文件
ios这边使用命令:
cargo lipo --release
去生成一个universal的通用文件(适用于ios模拟器和真机架构),会在你的rust目录里面找到一个.a结尾的静态文件。如果有一些dylib相关的报错,则可以先将Cargo.toml中的配置:
crate-type = ["staticlib", "cdylib"]
改为
crate-type = ["staticlib"]
即只编译静态库。
1.4 可执行文件链接到Flutter项目
- Android端在编译得到三个架构下的.so文件以后,如果rust代码没有任何改动,在build.gradle中加入的编译指令就不需要了,不然每次运行app的时候都会重新编译rust代码。
- ios端是需要添加一些xcode配置,以便每次运行时能够被找到并且链接到app中。
1.4.1 Android配置
安卓这边在执行build.gradle的编译指令时会将输入自动copy到jinLibs的对应架构的位置。无需做额外配置。
1.4.2 IOS配置
ios这边需要在xcode中进行配置,在xcode中打开项目,在Build Phases -> Link Binary With Libraries中添加.a文件,这一步可以事先把.a文件复制到ios目录下,因为在实际链接以后,我在运行项目并且调用时时常产生找不到的情况。
另外图片中链接了多个.a文件,这个是对依赖库的一个处理,较为简单的rust项目可以忽略,只链接刚刚编译出来的.a文件即可。
1.5 运行和调用
启动app以后,调用rust方法,就需要用到ffi的方式。
在flutter的pubspec.yaml中添加配置:
dependencies:
ffi: 2.1.3
flutter_rust_bridge: 1.82.6 # 用于自动生成ffi,如果采用手写方式则不需要
注意如果采用自动生成方式,flutter_rust_bridge的版本号要和rust项目中的Cargo.toml中的对应。
1.5.1 FFI方式
一、自动生成ffi
在flutter项目目录下运行:
flutter_rust_bridge_codegen -r rust/src/api.rs -d lib/ffi/rust_ffi/rust_ffi.dart -c ios/Runner/bridge_generated.h
记得把-r后面的路径改为自己rust项目的入口文件的路径,-d后面的路径为生成的dart ffi文件的路径,最后-c后面是生成c语言的头文件,这个也需要配置到ios的项目中去,里面列出了rust 库导出的符号,这样可以使xcode在编译项目时不会把这些符号去除。
在我的项目场景中是手写ffi的形式,没有使用到这个头文件,且运行中也没有产生问题,所以关于头文件的配置可以参考这一篇博客:
Flutter和Rust如何优雅的交互
二、调用
import 'dart:ffi';
import 'dart:io';
/// 用于读取并返回链接到项目中的可执行文件
class NativeFFI {
NativeFFI._();
static DynamicLibrary? _dyLib;
static DynamicLibrary get dyLib {
if (_dyLib != null) return _dyLib!;
const base = 'rust';
if (Platform.isIOS) {
_dyLib = DynamicLibrary.process();
} else if (Platform.isAndroid) {
_dyLib = DynamicLibrary.open('lib$base.so');
} else {
throw Exception('DynamicLibrary initialization failed');
}
return _dyLib!;
}
}
class NativeFun {
static final _ffi = RustFfiImpl(NativeFFI.dyLib); // RustFfiImpl为第一步中由flutter_rust_bridge生成的ffi
static Future<int> add(int left, int right) async {
int sum = await _ffi.add(left: left, right: right);
return sum;
}
}
在需要调用rust方法的地方,调用:
final sum = await NativeFun.add(1, 2);
print(sum);
将会输出3,表明成功调用了rust。到这一步为止可以认为整个流程成功。
如果是手写的ffi,即不用flutter_rust_bridge去生成RustFfiImpl,则把这一部分替换成写好的ffi即可。
2 依赖库处理
由于我这边实际情况下要引入的rust项目要复杂一些,rust编写的函数中涉及到openssl等库,所以实际调用的时候会报错找不到method,最后发现是依赖库没有和rust一起编译成二进制可执行文件。
尝试了两种方法:
- 在Cargo.toml中添加依赖库,企图一起编译成一个.so或者.a文件,但是最终失败了。
- 将依赖库的.so或者.a文件和rust编译的文件一起链接到项目中,最后成功,步骤和1.4中的步骤相同。
2.1 Openssl库的处理
Openssl库,可以clone源文件自行编译成.so或者.a文件(比较麻烦一些),也可以在github去找编译好的现成的文件。
安卓这边提供一个网址:下载地址 安卓三个架构均要下载。
2.1.1 Android
在安卓中,将libcrypto.so,libssl.so和之前编译好的librust.so放置在一个位置,即jinLibs下对应架构的文件夹里
2.1.2 IOS
ios这边可以从github中去找现成的编译项目,这边需要注意的是,.a文件需要使用fat文件,即支持i386和arm64两种架构的通用文件。
采用1.4中的方法在xcode中进行链接。
我这边需要链接这些:
-
报错1:链接以后还是会报一个flate2找不到的错误,解决方法是在Cargo.toml中添加这个库,在编译rust的时候编译进去:
[dependencies] flate2 = "1.0"
-
报错2:运行中遇到zlib的报错,解决方法是在rust项目的根目录新建一个build.rs文件,写如下内容:
fn main() { // Link zlib when build. println!("cargo:rustc-link-lib=static=z"); }
这样去辅助找到这个zlib库。
3 编译文件压缩
在实际应用场景,链接这些可执行文件以后,打包出的安装包大小会增大,所以需要降低编译文件的大小来缩小安装包大小。需要在Cargo.toml中添加如下配置:
[profile.release]
lto = true
opt-level = 'z'
strip = true
codegen-units = 1
这样的话release模式生成的.so和.a文件会有一定程度的压缩。