本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
本文主要是关于如何在 Android 中接入 Rust 的扫盲文,并不会对 Rust 做过多的讲解,对 Rust 有兴趣的可以自己去学习 Rust,至于如何才能快速的学会 Rust,可以参考我的这篇文章:《如何快速的掌握一门编程语言》
Rust 的前景
可能会有人担心花了较多的时间成本去学习了 Rust 后没啥使用的场景,或者学习后很快就过时了,其实这个大可不必担心,Rust 是非常有前途的一门语言。而且有越来越多的大公司开始采用 Rust 用于关键系统和新项目的开发。比如 Mozilla 用 Rust 来开发浏览器引擎,vivo 用 Rust 开发蓝河操作系统,字节的飞书、Tiktok 客户端也有大范围的使用使用 Rust,并且 Rust 的社区非常活跃,拥有丰富的库和工具支持,这意味着 Rust 的轮子很多,我们可以用 Rust 实现很多事情。因此学会 Rust 是很有性价比的一件事。
为什么越来越多项目开始用 Rust 呢?原因有很多,比如他性能好,可以媲美 c++;他的安全性高,像内存问题、线程安全问题,错误捕获问题在 Rust 中都能被很好的解决;它的语言较新,有很多现代编程语言的特性以及很多语法糖的支持,可以让代码更简洁和简单......大家可以在实际使用过程中慢慢的探索这款语言的优点。
Rust 的接入和使用
我这里以一个简单的 Demo 来讲解 Android 中如何接入和使用 Rust。
初始化
首先是下载和初始化,通过 curl 脚本即可快速下载 Rust。
arduino
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Rust 下载完成之后通过在命令行窗口执行脚本 "cargo new study_rust"就能新建一个项目名为 study_rust 的项目。通过 IDE 打开这个项目后的界面如下,我是用 clion 这个 ide 打开的,大家用 androidstudio 或者其他 ide 都可以,不过可能要下载 Rust 支持的插件。
在这项目的命令窗口中,执行 "cargo run"",就能成功的将该项目运行起来。
跨端通信
我们接下来就要看看如何在 Android 中调用 Rust。不同语言之间的互相调用,都要通过 FFI (Foreign function interface),也就是外部函数接口。
IOS 和 Rust 之间的 FFI 接口是用的 RustBridge,对 Android 来说,FFI 接口就是我们熟知的 JNI(Java Native Interface),可以看到 JNI 不仅可以用在 Java 调用 c 或 c++,还可以用来调用 Rust,所以我们一定要熟悉 JNI 的相关知识,这是 Android 进阶的必须要掌握的知识之一。
我们接着来看 Rust 中如何使用 JNI,首先在 Rust 项目的依赖配置中引入 JNI 库,如下所示。此时我们就可以使用 JNI 了。
我们接着在代码中创建 JNI 函数,规则和我们调用 C++的 JNI 函数是一样的,Java 下划线+包名+类名+方法名,包括传参规则也是一样,比如 JNI 中的 int 类型我 jint,String 类型为 jstring。在下面的 Demo 中,我创建了一个 add 方法的 JNI 函数,实现两数相加并返回结果这个简单的逻辑。
并且基于架构考虑,我将 add 方法放在了另外一个模块中,如下所示,放在lib.rs文件中。
这个时候我们用 Rust 写的的逻辑就实现了。接下来就需要把这个项目打包成 so 库文件
打包成 so 文件
首先在 Rust 项目的根目录创建 .cargo/config 文件,因为我在 demo 中只打 64 位的库,所以我只需要配置 target.aarch64-linux-android 的 ndk 路径就好了,我们需要配置 linker 和 ar 这两个工具的路径,这两个工具的说明如下:
- linker:用于链接二进制文件的工具。链接器负责将编译生成的目标文件(object files)合并成一个最终的可执行文件或共享库(.so 文件)。
- ar:指定用于创建和修改存档文件(如静态库)的工具。它负责将多个目标文件打包成一个单独的归档文件。
在我们的 NDK 文件的 toolchains/llvm/prebuilt/darwin-x86_64/bin 目录下有这两个工具。我使用的是 25.0.8775105 这个 NDK 版本,所以配置径如下。
如果我们需要生成 32 位 so 木架,则需要新增 [target.armv7-linux-androideabi]配置,并将 linker 指向同样的 NDK 目录下 armv7a-linux-androideabi21-clang 文件,ar 指向 arm-linux-androideabi-ar 文件。
配置好 linker 和 ar 后,在命令窗口中执行 "rustup target add aarch64-linux-android" 指令,用于为 Rust 项目添加一个 64 位 so 库的编译目标。因为 Rust 编译器默认情况下只为当前主机平台编译代码,如果想为其他平台编译 Rust 代码,就需要添加相应的编译目标,如果要编译 32 位的 so,则需要继续执行 "rustup target add x86_64-linux-android" 指令。
最后在命令窗口中执行 "cargo build --target aarch64-linux-android --release" 执行,就可以生成 64 位的 release 版本的 so 库了。生成的 so 文件在 target/aarch64-linux-anroid/release 目录中,如下图所示:
Android 接入
成功构建 so 库后,Android 侧接入就比较简单了,我这里就以一个很简单的 demo 来进行接入,如下所示,Rust 构建出来的 so 库的加载和 native 函数的声明与我们使用 c++ 生成的 so 库没任何区别。
然后在 log 中调用该 Rust 函数
最后运行程序,可以看到成功的实现了对 Rust 的 add 函数的调用。
应用场景
通过这个简单的案例,我们就知道了如何在 Android 项目中接入 Rust,可以看到其实并不复杂,所以我们可以大胆的用起来,那么 Rust 有哪些应用场景呢?
- 首先我们可以把原本需要用 c++ 实现的 Native 代码都用 Rust 来实现,Rust 完全可以替代 c++,它的性能不弱于 c++,并且安全性更高,也有大量的语法题支持,写起来更顺手。
- 我们可以所有数据相关的逻辑都放在 Rust 中处理,比如网络请求,数据库的读写,因为 Rust 支持协程,所以通过 Rust 的协程来进行数据操作,对性能也能有极大的提升。
- 有安全要求的代码可以用 Rust 来实现,上层中 Java 或者 Kotlin 写的代码通过反编译都能很容易的看到业务代码逻辑,但是通过 Rust 打包出来的 so 库就没法看到代码实现了。
- 对稳定性有高要求的业务可以用 Rust 来实现,因为足够的安全性就是 Rust 语言最有价值的特性之一。
- 跨平台的需求,通过一套Rust我们可以打包多个平台的库
- 其他如复杂的计算逻辑代码,对性能有高要求的代码等等都可以用 Rust 来实现。
架构设计
上面讲的只是一个简单的接入案例,通过这个案例我们初步了解了如何在 Android 中接入 Rust,在前面我们也知道了,Android 中可以使用 Rust 的场景非常的多,我们甚至可以把除 UI 渲染以外的所有逻辑都放在 Rust 侧来做。所以在实际项目中,我们还需要设计好 Rust 层的架构,只有一个好的项目架构,才能满足在项目中大规模的使用 Rust。
想要在项目中大规模的使用 Rust,我们主要需要解决大量 JNI 接口的问题,如果我们为每个业务都去新增一个 JNI 接口,那么 JNI 接口就会非常的庞大,而且我们在项目开发中也会变得非常繁琐。如下图所示,随着项目越来越大,Rust 的使用越来越多,JNI 接口也越来越庞大和臃肿,其次我们需要解决Rust中各个业务之间的解耦问题。
要如何解决这些问题呢?我们可以通过事件分发机制+Protobuf 数据协议来设计 Rust 层的架构,从而解决上面出现的问题。
具体的架构设计如下图所示,我们只需要定义一个 invoke 函数的 JNI 接口,上层在进行 invoke 接口调用时,只需要和 Rust 层定义好 command 接口,然后通过 ProtoBuf 来封装需要传给 Rust 层的数据,Rust 在执行完逻辑后,也将上层需要的数据结果以 ProtoBuf 进行封装后进行返回。
在 Rust 层中,当 invoke 函数被调用时,会将 command 和数据传递给 looper,looper 是我们在 Rust 层设计的一个观察者模式的数据分发器,各个业务需要注册 command 以及 command 对应的数据回调接口调到 looper 的 servicemap 中,loooper 解析 invoke 函数传递过来的 command,然后将数据回调给该 command 对应的业务。
如果业务越来越复杂,一个 invoke 接口依然是不够用的,我们可以根据业务需要再扩展几个接口,比如 init 接口,专门用于初始化,invokeSync 用于同步调用,invokeAsync 用于异步调用,如果是异步调用,则需要传入一个 jobject 类型的 callback 函数,用于业务在执行完逻辑后通过 callback 进行异步回调。架构设计如下
同步调用我们很容易理解,直接在方法逻辑执行完成后返回数据即可,在前面演示的简单的 Rust 接入的 demo 中,我们可以看到 Rust 中 add 函数在末尾返回的函数可以直接传递给上层。异步调用我在这里专门解释下如何使用,其实这也是 JNI 的知识,我们首先需要再 Rust 层定义一个 Callback 接口
java
// RustCallback.java
package com.example.myapp;
public interface RustCallback {
void onResponse(byte[] response);
}
然后在 invokeAsync 这个 JNI 函数中传递 callback 接口,传递的类型是 jobject 类型。在 c++ 或 Rust 层中,在通过 env->GetMethodID 获取该 callback 的 method_id,最后通过 env.call_method 方法既能实现对上层的 callback 回调,代码实现如下:
rust
pub extern "C" fn Java_com_example_myapp_RustBridge_invokeAsync(
env: JNIEnv,
_: JClass,
command:jint,
data:jbyterArray,
java_callback: jobject
) {
let env = env.clone();
let java_callback = env.new_global_ref(java_callback).unwrap();
let runtime = RUNTIME.clone();
runtime.lock().unwrap().spawn(async move {
//逻辑处理并封装respond数据
......
//通过env获取回调接口的method_id
let method_id = env.get_method_id(
"com/example/myapp/RustCallback",
"onResponse",
"([B)V",
).unwrap();
//callback回调ProtoBuf协议的数据
env.call_method(
java_callback.as_obj(),
method_id,
result_data,
).unwrap();
});
}
通过上面的演示,我们了解到了 JNI 的异步回调的使用,实际上,我们可以在Rust的 init 函数中就将 java_callback 传递下来,然后将获取的 java_callback 的 method_id 设置成全局常量,这样后续流程中上层和 Rust 层在进行异步调用的时候,也可以不需要传 callback 这个参数,简化了调用流程,并且在 Rust 方法逻辑中也不需要再重复获取该 callback 的 method_id,直接使用全局的 method_id 即可。
此时也许会有人担心上层那么多业务用同一个 callback 不会有问题么?实际上在上层我们也可以设计一个消息时机分发机制,虽然所有的 Rust 的异步调用的数据都通过这个 callback 返回,但是上层 callback 在收到回调的数据后,通过 command 以及订阅者模式机制去进行分发。所以一个完整的架构流程如下所示:
到这里我就讲完了如何在 Android 中使用 Rust 了,大家可以尽量去尝试和使用,亲自体验一下 Rust 的魅力!