Flutter + Rust 高效能的跨端嘗試

Heymind發表於2020-02-15

稍作配置,同一份程式碼橫跨 Android & IOS,相比於 React Native 方案更加高效能。除此之外,得益於 Rust 跨平臺加持,Rust 部分的程式碼可在種種場合複用。

這篇文章旨在記錄作者嘗試結合 Rust 和 Flutter 的過程,且僅為初步嘗試。不會涉及諸如:

  • 如何搭建一個 Flutter 開發環境,以及 Dart 語言怎麼用
  • 如何搭建一個 Rust 開發環境,以及 Rust 語言怎麼學

Environment

  • Flutter: Android, IOS 工具配置妥當
    -w672
  • Rust: Stable 就好
    -w513

Rust Part

Prepare cross-platform toolchains & deps

IOS

# Download targets for IOS ( 64 bit targets (real device & simulator) )
rustup target add aarch64-apple-ios x86_64-apple-ios 

# Install cargo-lipo to generate the iOS universal library
cargo install cargo-lipo
複製程式碼

Android

這裡有一些行之有效的輔助指令碼用於更加快捷配置交叉編譯工具。

  1. 獲取 Android NDK

    sdkmanager --verbose ndk-bundle
    複製程式碼

    如果已經準備好了 Android NDK ,則設定環境變數 $ANDROID_NDK_HOME

    # example:
    export ANDROID_NDK_HOME=/Users/yinsiwei/Downloads/android-ndk-r20b
    複製程式碼
  2. Create the standalone NDK

    # $(pwd) == ~/Downloads
    git clone https://github.com/kennytm/rust-ios-android.git
    cd rust-ios-android
    ./create-ndk-standalone.sh
    複製程式碼
  3. 在 Cargo default config VS 配置 Android 交叉編譯工具

    cat cargo-config.toml >> ~/.cargo/config
    複製程式碼

    執行上述命令後會在 Cargo 預設配置中,增加有關 Android 跨平臺目標 (targets, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android) 的工具資訊,指向剛剛建立的 standalone NDK

    [target.aarch64-linux-android]
    ar = ...
    linker = ..
    
    [target.armv7-linux-androideabi]
    ...
    
    [target.i686-linux-android]
    ..
    複製程式碼
  4. 下載 Rust 支援 Android 交叉編譯的依賴

    複製程式碼

rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android ```

Start a simple rust library

  1. 建立一個 Rust 專案

    複製程式碼

cargo init my-app-base --lib ```

  1. 編輯 Cargo.toml 修改 crate-type

    複製程式碼

[lib] name = "my_app_base" crate-type = ["staticlib", "cdylib"] ``` Rust 構建出來的二進位制庫,在 IOS 中是靜態連結進最終的程式之中,需要對構建 staticlib 的支援;在 Android 是通過動態連結在執行時裝在程式序執行空間的,需要對構建 cdylib 的支援。

  1. 寫一些符合 C ABI 的函式 src/lib.rs

    use std::os::raw::c_char;
    use std::ffi::CString;
    
    #[no_mangle]
    pub unsafe extern fn hello() -> *const c_char {
        let s = CString::new("world").unwrap();
        s.into_raw()
    }
    複製程式碼

    在上述程式碼中,每次當外部呼叫 hello 函式時,會在晉城堆空間中建立一個字串 ( CString ),並將所有權 ( 釋放該字串所佔堆空間的權利 ) 移交給呼叫者

Build libraries

# IOS
cargo lipo --release

# Android
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release
複製程式碼

然後在 target 目錄下會得到以下有用的物料。

target
    ├── aarch64-linux-android
    │   └── release
    │       ├── libmy_app_base.a
    │       └── libmy_app_base.so
    ├── armv7-linux-androideabi
    │   └── release
    │       ├── libmy_app_base.a
    │       └── libmy_app_base.so
    ├── i686-linux-android
    │   └── release
    │       ├── libmy_app_base.a
    │       └── libmy_app_base.so
    ├── universal
    │   └── release
    │       └── libmy_app_base.a
複製程式碼

至此, Rust 部分就告於段落了。

Flutter Part

Copy build artifacts to flutter project

from: target/universal/release/libmy_app_base.a 
to: ios/

from: target/aarch64-linux-android/release/libmy_app_base.so 
to: android/app/src/main/jniLibs/arm64-v8a/

from: target/armv7-linux-androideabi/release/libmy_app_base.so 
to: android/app/src/main/jniLibs/armeabi-v7a/

from: target/i686-linux-android/release/libmy_app_base.so 
to: android/app/src/main/jniLibs/x86/
複製程式碼

Call FFI function in Dart

  1. 新增依賴

    pubspec.yaml -> dev_dependencies: += ffi: ^0.1.3

  2. 新增程式碼

    (直接在生成的專案上修改,暫不考慮程式碼設計問題,就簡簡單單的先把專案跑起來 )

    import 'dart:ffi';
    import 'package:ffi/ffi.dart';
    
    // ...
    final dylib = Platform.isAndroid ? DynamicLibrary.open('libmy_app_base.so') :DynamicLibrary.process();
    var hello = dylib.lookupFunction<Pointer<Utf8> Function(),Pointer<Utf8> Function()>('hello');
    
    // ...
    hello(); 
    // -> world
    複製程式碼

Build Android Project

flutter run # 如果連線著 Android 裝置就直接執行了起來
複製程式碼

Build IOS Project

( 複雜了許多 )

  1. 跟隨 Flutter 官方文件,配置 XCode 專案。
  2. Build PhasesLink Binary With Libraries 新增 libmy_app_base.a 檔案 (按照圖上箭頭點...)
    -w1140
  3. Build SettingsOther Linker Flags 中新增 force_load 的引數。
    -w855

這是由於在 Dart 中通過動態的方式呼叫了該庫的相關函式,但在編譯期間靜態分析的時候,這些都是未曾被呼叫過的無用函式,就被剪裁掉了。要通過 force_load 方式解決這個問題。

Result

2020-02-15 12.39.59-w300

ezgif-6-785f61b1b53b

Troubleshooting

XCode & IOS

Error getting attached iOS device: ideviceinfo could not find device

sudo xattr -d com.apple.quarantine ~/flutter/bin/cache/artifacts/libimobiledevice/ideviceinfo
複製程式碼

將後面的路徑替換成你的

dyld: Library not loaded

dyld: Library not loaded: /b/s/w/ir/k/homebrew/Cellar/libimobiledevice-flutter/HEAD-398c120_3/lib/libimobiledevice.6.dylib
  Referenced from: /Users/hey/flutter/bin/cache/artifacts/libimobiledevice/idevice_id
  Reason: image not found
複製程式碼

刪除&重新下載

rm -rf /Users/hey/flutter/bin/cache && flutter doctor -v
複製程式碼

真機無法啟動 Flutter 程式

參見 github.com/flutter/flu… 不要升級到 IOS 13.3.1 系統

What's next

  • 如何高效的實現 Rust & Dart 部分的通訊

    我們知道 Flutter 和廣大 GUI 庫類似,屬於單執行緒模型結合事件系統,因此在主執行緒中使用 FFI 呼叫 Rust 部分的程式碼不能阻塞執行緒。Dart 語言提供 async/await 語法特性用於在 Flutter 中處理網路請求等阻塞任務。而 Rust 也在最近版本中提供了 async/await 語法支援,如何優雅的把兩部分結合起來,這是一個問題。

  • 對 MacOS Windows Linux 桌面端的支援

    Flutter 已經有了對桌面端的實驗性支援,可以研究下如何結合在一起,實現跨 6 個端共享程式碼。

References


博文地址: idx0.dev/2020/02/15/…

相關文章