大家好,我是 imike! ! !

Swift 6 顛覆性的 Android NDK 支援終於讓我可以發布JNIKit了,這是我自 Swift 5 以來一直在為SwifDroid專案建置的便捷工具!現在最大的障礙已經消失:我們可以直接import Android ,而不必費力地手動匯入頭檔。雖然最後一步,也就是官方的二進位檔案生成,仍然由finagolfin出色的swift-android-sdk (Swift Stream 使用的工具)負責,但 Swift 專案已經計劃將其作為官方 SDK。

今天,我想向你展示如何為 Android 編寫你的第一個真正的原生 Swift 程式碼。這將會是一段有趣的旅程,所以,準備好一杯茶,讓我們開始吧。

您需要什麼:

  1. Docker

  2. 帶有Dev Containers 擴充功能的VSCode

  3. VSCode 的Swift Stream IDE擴展

或者,安裝 Android Studio,以便稍後使用真正的 Android 應用程式測試您的程式庫。

只要您的作業系統可以執行 Docker 和 VSCode,它就不重要。

安裝 Docker 後,開啟 VSCode。

首先,請確保您已安裝Dev Containers 擴充功能

安裝了 DevContainers 擴充的 VSCode

接下來,確保Swift Stream IDE擴充也已安裝。

圖片描述

如果您還沒有這些擴展,只需在市場上搜尋它們並點擊Install (您的 Captain Obvious 😄)

建立新專案

在 VSCode 的左側邊欄上,按一下 Swift Stream 圖示(小鳥)。

圖片描述

....然後點選Start New Project 😏

現在,輸入您的專案名稱,例如MyFirstAndroidLib

圖片描述

您將看到新專案預設會建立在您的主資料夾中。您可以點擊三個點按鈕來選擇其他資料夾。

下一步是選擇專案類型。對我們來說,是Android -> Library

圖片描述

按一下Create Project

接下來,輸入庫的 Java 命名空間。這通常是網域的反向名稱(例如com.example.mylib )。

圖片描述

輸入命名空間後,按 Enter 進入下一步,其中選擇Android Min SDK Version

圖片描述

我建議根據您的需求選擇2429 。再次按Enter選擇Android Compile SDK Version

圖片描述

就目前而言, 35是一個不錯的選擇。再次按下Enter ,開始建立專案。

此時,VSCode 將建立一個包含所有專案檔案的資料夾,並開始下載具有可立即使用的 Android 開發環境的 Docker 映像。

下載鏡像後,它將啟動容器並開啟一個新的 VSCode 窗口,其中包含您的專案。然後,容器將下載 Swift 工具鏈、Swift for Android SDK、Gradle、Android NDK 和 Android SDK。這些工具會快取在共享的 Docker 磁碟區中,因此您的下一個專案將在幾秒鐘內建立完成。但是,首次啟動可能需要一些時間,請耐心等待。

圖片描述

一切就緒!可以開始寫程式了!

前言

什麼是JNI

Java 原生介面 (JNI) 是原生程式碼與 Java 虛擬機器通訊的橋樑。具體來說:如果您編寫的是 Java 程式碼,則可以使用 Android SDK。但如果您使用的語言(例如 Swift 或 C++)無法編譯為 Java 字節碼,則需要 Android NDK 透過 JNI 與 Java 進行通訊。

使用 JNI,您幾乎可以完成任何用 Java 可以完成的工作 — — 真正的挑戰是以一種不那麼令人頭疼的方式來完成它。

JNIKit 是什麼

這正是 JNIKit 的用武之地!為了舒適高效地執行,我們需要一個便捷的層,將那些低階的 C 風格 JNI 呼叫包裝成更優雅、更 Swift 風格的程式碼。這正是 JNIKit 的用武之地。

專案

結構

從本質上講,它是一個純粹的 Swift 套件管理器專案。關鍵依賴項是JNIKit以及帶有swift-logAndroidLogging

您的 Swift 程式碼預設位於Sources/<target_name>/Library.swift中。

Android 函式庫(一個 Gradle 專案)位於Library資料夾中。首次 Swift 建置後,此資料夾將自動產生。或者,您也可以從 Swift Stream 側邊欄面板手動建立它。

Swift 程式碼

一切都始於一個initialize方法。此方法暴露給 Java/Kotlin 端,必須在任何其他原生方法之前呼叫。

以下程式碼顯示如何使用@_cdecl為 JNI 公開此方法。

@_cdecl命名約定至關重要,因為它遵循 JNI 模式:

Java_<package>_<class>_<method>
  • package是使用底線而不是點的完全限定套件名稱

  • class是類別名稱

  • method是方法名稱

此方法的參數也遵循 JNI 約定。前兩個參數是必需的,由 JNI 自動傳遞:

  1. envPointer :這個指標永遠不會改變。它是指向 JNI 環境的指針,是與 JVM 互動的主要介面。

  2. clazzRefthizRef :如果 Java 方法是靜態的(例如本例中,該方法位於 Kotlin object內部),則傳回clazzRef 。如果是實例方法,則傳回thizRef 。前者是指向類別的指標;後者是指向實例的指標。

這些參數之後的任何參數都代表 Java/Kotlin 方法本身的參數。在我們的例子中,該方法有一個額外的參數:一個caller物件。我們從應用程式中傳遞它來提供上下文。這個caller實例對於快取應用程式的類別載入器(稍後會詳細介紹)是必需的。注意:如果我們使用的是thizRef而不是clazzRef ,我們可能不需要傳遞這個額外的caller物件。

#if os(Android)
@_cdecl("Java_to_dev_myandroidlib_myfirstandroidproject_SwiftInterface_initialize")
public func initialize(
    envPointer: UnsafeMutablePointer<JNIEnv?>,
    clazzRef: jobject,
    callerRef: jobject
) {
    // Activate Android logger
    LoggingSystem.bootstrap(AndroidLogHandler.taggedBySource)
    // Initialize JVM
    let jvm = envPointer.jvm()
    JNIKit.shared.initialize(with: jvm)
    // ALSO: class loader cache example
    // ALSO: `toString` example
    // ALSO: `Task` example
}
#endif

方法主體顯示我們首先使用 Android 記錄器引導 Swift 日誌系統(這只需要執行一次)。

之後,我們可以在任何地方使用記錄器,就像這樣:

let logger = Logger(label: "🐦‍🔥 SWIFT")
logger.info("🚀 Hello World!")

然後,我們初始化與 JVM 的連線。現在,一切就緒!

類別載入器和快取

這裡有一個常見的問題:預設情況下,當您嘗試透過 JNI 尋找 Java 類別時,它會使用系統類別載入器。這個系統載入器(驚喜吧!)無法辨識您應用程式中動態載入的類別——這意味著它會錯過您自己的類別以及任何 Gradle 依賴項。

解決方案?我們需要取得應用程式的類別載入器,它可以透過.getClass().getClassLoader()從任何 Java 物件取得。最佳實踐是在初始化期間獲取該類別載入器一次,建立對它的全域引用,將其儲存在 JNIKit 的快取中,並在任何地方使用它。它在整個應用程式生命週期內都有效。

以下是在initialize方法中快取的方法:

// Access current environment
let localEnv = JEnv(envPointer)
// Convert caller's local ref into global ref
let callerBox = callerRef.box(localEnv)
// Defer block to clean up local references
defer {
    // Release local ref to caller object
    localEnv.deleteLocalRef(callerRef)
}
// Initialize `JObject` from boxed global reference to the caller object
guard let callerObject = callerBox?.object() else { return }
// Cache the class loader from the caller object
// it is important to load non-system classes later
// e.g. your own Java/Kotlin classes
if let classLoader = callerObject.getClassLoader(localEnv) {
    JNICache.shared.setClassLoader(classLoader)
    logger.info("🚀 class loader cached successfully")
}

注意:如果您的本機方法是實例方法,則應使用thizRef而不是callerRef

我可以使用 Java 的toString()嗎?

是的,當然!這是一個至關重要的 Java 方法,JNIKit 讓它的使用變得非常簡單:

logger.info("🚀 caller description: \(someObject.toString())")

另一個線程上的環境

JNIEnv與目前執行緒綁定。這個環境就像一座橋樑,負責完成所有操作,將呼叫傳遞到 JVM 或從 JVM 中傳出。

如果切換線程(例如,在Task中),則必須將 JNI 環境附加到該新線程。 JNIKit 為此提供了一個簡單的方法: JEnv.current()

Task {
    // Access current environment in this thread
    guard let env = JEnv.current() else { return }
    logger.info("🚀 new env: \(env)")
    // Print JNI version into LogCat
    logger.info("🚀 jni version: \(env.getVersionString())")
}

程式碼在另一側看起來如何

Java

public final class SwiftInterface {
    static {
        System.loadLibrary("MyFirstAndroidProject");
    }
    private SwiftInterface() {}
    public static native void initialize(Object caller);
}

科特林

object SwiftInterface {
    init {
        System.loadLibrary("MyFirstAndroidProject")
    }
    external fun initialize(caller: Any)
}

Swift Stream 會為你產生 Kotlin 文件,所以我們就堅持這樣做。稍後我們會看到更多 JNI 範例 🙂

建立 Swift 專案

好了,開始建造!切換到左側邊欄的Swift Stream選項卡,然後點選Project -> Build

圖片描述

系統將提示您選擇DebugRelease方案。

圖片描述

現在讓我們進行Debug 。建置過程即將開始。

在 Swift Stream 中,您可以選擇Log Level來控制看到的詳細資訊量:

  • 普通的

  • 詳細(這是預設設定)

  • 詳細

  • 難以忍受(當你真的需要看到一切時)

使用預設的詳細級別,您將在建置過程中看到類似如下的輸出:

🏗️ Started building debug
💁‍♂️ it will try to build each phase
🔦 Resolving Swift dependencies for native
🔦 Resolved in 772ms
🔦 Resolving Swift dependencies for droid
🔦 Resolved in 2s918ms
🧱 Building `MyFirstAndroidProject` swift target for arm64-v8a
🧱 Built `MyFirstAndroidProject` swift target for `.droid` in 10s184ms
🧱 Building `MyFirstAndroidProject` swift target for armeabi-v7a
🧱 Built `MyFirstAndroidProject` swift target for `.droid` in 7s202ms
🧱 Building `MyFirstAndroidProject` swift target for x86_64
🧱 Built `MyFirstAndroidProject` swift target for `.droid` in 7s135ms
🧱 Preparing gradle wrapper
🧱 Prepared gradle wrapper in 1m50s
✅ Build Succeeded in 2m20s

如您所見,初始 Swift 編譯本身非常快,三個架構目標(arm64-v8a、armeabi-v7a 和 x86_64)總共耗時約30 秒。大部分時間(1 分 50 秒)花在了初始gradle wrapper設定上,這是一次性開銷。

好訊息是,後續建置速度會非常快,三個目標只需大約 3 秒!這是因為所有內容都被快取了。

此建置命令也會自動為您產生 Java 庫 Gradle 專案。現在,它已準備好在Library資料夾中使用。

Java/Kotlin 專案

原始碼

Swift Stream 為您的程式庫產生初始樣板程式碼,然後您可以維護和擴充它。

以下是產生的 Kotlin 介面的範例:

import android.util.Log

object SwiftInterface {
    init {
        System.loadLibrary("MyFirstAndroidProject")
    }

    external fun initialize(caller: Any)

    external fun sendInt(number: Int)
    external fun sendIntArray(array: IntArray)
    external fun sendString(string: String)
    external fun sendDate(date: Date)
    external fun ping(): String
    external fun fetchAsyncData(): String
}

Gradle 文件

Swift Stream IDE 會自動管理您的 Gradle 專案。它會根據Package.swift中的 Swift 目標產生 Java 包,並保持所有 Gradle 檔案同步。

Library/settings.gradle.kts中,它管理特殊註釋標籤中包含的目標清單:

// managed by swiftstreamide: includes-begin
include(":myfirstandroidproject")
// managed by swiftstreamide: includes-end

在每個Library/<target>/build.gradle.kts檔案中,它會根據 Swift 程式碼中的匯入和您正在使用的 Swift 版本自動管理依賴項:

implementation("com.github.swifdroid.runtime-libs:core:6.1.3")
// managed by swiftstreamide: so-dependencies-begin
implementation("com.github.swifdroid.runtime-libs:foundation:6.1.3")
implementation("com.github.swifdroid.runtime-libs:foundationessentials:6.1.3")
implementation("com.github.swifdroid.runtime-libs:i18n:6.1.3")
// managed by swiftstreamide: so-dependencies-end

預設情況下,這些依賴項會自動從 SwifDroid runtime-libs JitPack 倉庫中獲取,該倉庫為每個支援的 Swift 版本維護。這意味著無需手動從 Android SDK 套件複製.so檔!

但是,如果您需要更多控制權,您可以手動接管,而無需手動複製文件的麻煩。 Swift Stream IDE 使用設定檔 ( .vscode/android-stream.json ),您可以在其中設定soMode

"soMode": "Packed"

"Packed" (預設)表示 Gradle 從 JitPack 匯入所有內容。您可以切換到"PickedManually"以僅指定您實際需要的.so檔:

"soMode": "PickedManually",
"schemes": [
    {
        "title": "MyFirstAndroidProject Debug",
        "soFiles": [
            "libandroid.so",
            "libc.so",
            "libm.so"
        ]
    }
]

此設定檔也是您控制其他關鍵專案設定的地方:

"packageName": "to.dev.myandroidlib",
"compileSDK": 35,
"minSDK": 24,
"javaVersion": 11,

您甚至可以將自訂參數直接傳遞給 Swift 編譯器以進行細微控制:

"schemes": [
    {
        "title": "MyFirstAndroidProject Debug",
        "swiftArgs": []
    }
]

使用 Gradle 進行組裝

最後,要建立可分發的 Android 庫檔案( .aar ),只需點擊 Swift Stream 側邊欄中的Java Library Project -> Assemble

圖片描述

此指令在背景執行gradlew assembleDebuggradlew assembleRelease ,將所有內容打包以供分發。

將此庫新增至您的 Android 專案(本地)

現在到了最有趣的部分,讓我們在一個真正的 Android 應用程式中使用這個函式庫!開啟你現有的專案,或是在 Android Studio 中建立一個新的專案。

打開專案後,第一步是新增 JitPack 作為倉庫。導航到你的settings.gradle.kts文件,並確保它包含 JitPack 倉庫:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        maven { url = uri("https://jitpack.io") }
        mavenCentral()
    }
}

接下來,您需要將依賴項新增至應用程式模組的build.gradle.kts檔案 ( app/build.gradle.kts )。您必須同時包含.aar檔案和所有必要的執行時庫:

dependencies {
    implementation(files("libs/myfirstandroidproject-debug.aar"))
    implementation("com.github.swifdroid.runtime-libs:core:6.1.3")
    implementation("com.github.swifdroid.runtime-libs:foundation:6.1.3")
    implementation("com.github.swifdroid.runtime-libs:foundationessentials:6.1.3")
    implementation("com.github.swifdroid.runtime-libs:i18n:6.1.3")
    // the rest of dependencies
}

重要提示:您必須手動列出這些依賴項,因為 Android 無法自動從.aar檔案內部取得它們。

取得.AAR文件

現在,取得你剛剛建立的庫檔案!你可以在 Swift Stream 專案的以下路徑找到.aar檔:

Library/myfirstandroidproject/build/outputs/aar/myfirstandroidproject-debug.aar

複製此文件。然後,在您的 Android Studio 專案中,導航到應用程式模組的目錄(例如app/ ),並在build.gradle.kts檔案旁邊建立一個名為libs的資料夾。將.aar檔案貼到這個新的libs資料夾中。

讓魔法開始吧! 🚀

一切就緒!現在,在應用程式的某個地方——通常是在Application類別或主 Activity 的onCreate中,初始化 Swift 程式碼:

SwiftInterface.initialize(this)

同步您的 Gradle 專案、建置它並在裝置或模擬器上執行它。

關鍵時刻:開啟 LogCat 並篩選「SWIFT」。你應該會看到我們這精彩的訊息:

 I  [🐦‍🔥 SWIFT] 🚀 Hello World!

耶!你的 Swift 程式碼現在可以在 Android 上執行了。

開發循環

當您對 Swift 程式碼進行更改時,這是您的快速更新週期:

  1. 在 Swift Stream 中,點擊Project -> Build

  2. 然後,點選Java Library Project -> Assemble

  3. 將新的.aar檔案從outputs/aar資料夾複製到 Android 專案的app/libs資料夾中,取代舊檔案。

  4. 重建並執行您的 Android 應用程式!

就這樣!你現在是跨平台 Swift 開發者了。

JNI 範例

現在到了最令人興奮的部分——程式碼!讓我們來談談如何在 Swift 和 Java/Kotlin 之間進行通訊。我們將繼續使用 Kotlin,因為它是當今 Android 開發的標準。

我們將在本文中介紹一些簡單但常見的場景,並在下次深入探討更複雜的場景。

⚠️至關重要:不要忘記在任何其他本機呼叫之前呼叫SwiftInterface.initialize(this)

從 Kotlin 向 Swift 發送Int

讓我們從簡單的開始。在SwiftInterface.kt中宣告一個方法:

external fun sendInt(number: Int)

在 Swift 端實現它:

#if os(Android)
@_cdecl("Java_to_dev_myandroidlib_myfirstandroidproject_SwiftInterface_sendInt")
public func sendInt(
    envPointer: UnsafeMutablePointer<JNIEnv?>,
    clazzRef: jobject,
    number: jint
) {
    let logger = Logger(label: "🐦‍🔥 SWIFT")
    logger.info("#️⃣ sendInt: \(number)")
}
#endif

從您的應用程式呼叫它:

SwiftInterface.sendInt(123)

檢查 LogCat:

 I  [🐦‍🔥 SWIFT] #️⃣ sendInt: 123

太簡單了,對吧? :)

從 Kotlin 向 Swift 發送IntArray

聲明方法:

external fun sendIntArray(array: IntArray)

在 Swift 端,處理陣列:

#if os(Android)
@_cdecl("Java_to_dev_myandroidlib_myfirstandroidproject_SwiftInterface_sendIntArray")
public func sendIntArray(
    envPointer: UnsafeMutablePointer<JNIEnv?>,
    clazzRef: jobject,
    arrayRef: jintArray
) {
    // Create lightweight logger object
    let logger = Logger(label: "🐦‍🔥 SWIFT")
    // Access current environment
    let localEnv = JEnv(envPointer)
    // Defer block to clean up local references
    defer {
        // Release local ref to array object
        localEnv.deleteLocalRef(arrayRef)
    }
    // Get array length
    logger.info("🔢 sendIntArray 1")
    let length = localEnv.getArrayLength(arrayRef)
    logger.info("🔢 sendIntArray 2 length: \(length)")
    // Get array elements
    var swiftArray = [Int32](repeating: 0, count: Int(length))
    localEnv.getIntArrayRegion(arrayRef, start: 0, length: length, buffer: &swiftArray)
    // Now you can use `swiftArray` as a regular Swift array
    logger.info("🔢 sendIntArray 3 swiftArray: \(swiftArray)")
}
#endif

從您的應用程式呼叫它:

SwiftInterface.sendIntArray(intArrayOf(7, 6, 5))

檢查 LogCat:

 I  [🐦‍🔥 SWIFT] 🔢 sendIntArray: 1
 I  [🐦‍🔥 SWIFT] 🔢 sendIntArray: 2 length: 3
 I  [🐦‍🔥 SWIFT] 🔢 sendIntArray: 3 swiftArray: [7, 6, 5]

從 Kotlin 向 Swift 發送String

聲明方法:

external fun sendString(string: String)

在 Swift 方面:

#if os(Android)
@_cdecl("Java_to_dev_myandroidlib_myfirstandroidproject_SwiftInterface_sendString")
public func sendString(envPointer: UnsafeMutablePointer<JNIEnv?>, clazzRef: jobject, strRef: jobject) {
    // Create lightweight logger object
    let logger = Logger(label: "🐦‍🔥 SWIFT")
    // Access current environment
    let localEnv = JEnv(envPointer)
    // Defer block to clean up local references
    defer {
        // Release local ref to string object
        localEnv.deleteLocalRef(strRef)
    }
    // Wrap JNI string reference into `JString` and get Swift string
    logger.info("✍️ sendString 1")
    guard let string = strRef.wrap().string() else {
        logger.info("✍️ sendString 1.1 exit: unable to unwrap jstring")
        return
    }
    // Now you can use `string` as a regular Swift string
    logger.info("✍️ sendString 2: \(string)")
}
#endif

從您的應用程式呼叫它:

SwiftInterface.sendString("With love from Java")

檢查 LogCat:

 I  [🐦‍🔥 SWIFT] ✍️ sendString 1
 I  [🐦‍🔥 SWIFT] ✍️ sendString 2: With love from Java

Date物件從 Kotlin 傳送到 Swift

聲明方法:

external fun sendDate(date: Date)

在 Swift 方面:

#if os(Android)
@_cdecl("Java_to_dev_myandroidlib_myfirstandroidproject_SwiftInterface_sendDate")
public func sendDate(envPointer: UnsafeMutablePointer<JNIEnv?>, clazzRef: jobject, dateRef: jobject) {
    // Create lightweight logger object
    let logger = Logger(label: "🐦‍🔥 SWIFT")
    // Access current environment
    let localEnv = JEnv(envPointer)
    // Defer block to clean up local references
    defer {
        // Release local ref to date object
        localEnv.deleteLocalRef(dateRef)
    }
    // Wrap JNI date reference into `JObjectBox`
    logger.info("📅 sendDate 1")
    guard let box = dateRef.box(localEnv) else {
        logger.info("📅 sendDate 1.1 exit: unable to box Date object")
        return
    }
    // Initialize `JObject` from boxed global reference to the date
    logger.info("📅 sendDate 2")
    guard let dateObject = box.object() else {
        logger.info("📅 sendDate 2.1 exit: unable to unwrap Date object")
        return
    }
    // Call `getTime` method to get milliseconds since epoch
    logger.info("📅 sendDate 3")
    guard let milliseconds = dateObject.callLongMethod(name: "getTime") else {
        logger.info("📅 sendDate 3.1 exit: getTime returned nil, maybe wrong method")
        return
    }
    // Now you can use `milliseconds` as a regular Swift Int64 value
    logger.info("📅 sendDate 4: \(milliseconds)")
}
#endif

從您的應用程式呼叫它:

SwiftInterface.sendDate(Date())

檢查 LogCat:

 I  [🐦‍🔥 SWIFT] 📅 sendDate 1
 I  [🐦‍🔥 SWIFT] 📅 sendDate 2
 I  [🐦‍🔥 SWIFT] 📅 sendDate 3
 I  [🐦‍🔥 SWIFT] 📅 sendDate 4: 1757533833096

在 Kotlin 中從 Swift 接收字串

聲明一個傳回值的方法:

external fun ping(): String

在 Swift 端,傳回一個字串:

#if os(Android)
@_cdecl("Java_to_dev_myandroidlib_myfirstandroidproject_SwiftInterface_ping")
public func ping(envPointer: UnsafeMutablePointer<JNIEnv?>, clazzRef: jobject) -> jobject? {
    // Wrap Swift string into `JSString` and return its JNI reference
    return "🏓 Pong from Swift!".wrap().reference()
}
#endif

從您的應用程式呼叫它:

Log.i("HELLO", "Pinging: ${SwiftInterface.ping()}")

檢查 LogCat:

 I  Pinging: 🏓 Pong from Swift!

從 Kotlin 執行 Async/Await Swift 程式碼

聲明方法:

external fun fetchAsyncData(): String

您需要知道@_cdecl屬性不適用於非同步運算子。這就是為什麼我們在這裡使用semaphore來以非同步的方式執行 Swift 程式碼。這種方法完全沒問題,但僅適用於non-UI程式碼。如果您在主線程上嘗試這樣做,您將面臨徹底的死鎖,所以不要這樣做。我將在下一篇文章中向您展示如何處理 UI 程式碼。

@_cdecl("Java_to_dev_myandroidlib_myfirstandroidproject_SwiftInterface_fetchAsyncData")
public func fetchAsyncData(env: UnsafeMutablePointer<JNIEnv>, obj: jobject) -> jstring? {
    // Create semaphore to wait for async task
    let semaphore = DispatchSemaphore(value: 0)
    // Create result variable
    var result: String? = nil
    // Start async task
    Task {
        // Simulate async operation
        try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds
        // Set result
        result = "Async data fetched successfully!"
        // Release semaphore
        semaphore.signal()
    }
    // Wait for async task to complete by blocking current thread
    semaphore.wait()
    // Check if result is available
    guard let result = result else { return nil }
    // Wrap Swift string into `JSString` and return its JNI reference
    return result.wrap().reference()
}
#endif

從您的應用程式呼叫它(脫離主執行緒!):

CoroutineScope(Dispatchers.IO).launch {
    Log.i("ASYNC", "Swift async call started")
    try {
        val result = SwiftInterface.fetchAsyncData()
        Log.i("ASYNC", "Swift returned: $result")
    } catch (e: Exception) {
        // Handle error
    }
    Log.i("ASYNC", "Swift async call finished")
}

檢查 LogCat:

 I  Swift async call started
 I  Swift returned: Async data fetched successfully!
 I  Swift async call finished

在 Swift 中包裝 Java 類

為了在 Swift 中使用 Java 類,我們需要包裝器。讓我們為java/util/Date建立一個:

public final class JDate: JObjectable, Sendable {
    /// The JNI class name
    public static let className: JClassName = "java/util/Date"

    /// JNI global reference object wrapper, it contains class metadata as well.
    public let object: JObject

    /// Initializer for when you already have a `JObject` reference.
    /// 
    /// This is useful when you receive a `Date` object from Java code.
    public init (_ object: JObject) {
        self.object = object
    }

    /// Allocates a `Date` object and initializes it so that it represents the time
    /// at which it was allocated, measured to the nearest millisecond.
    public init? () {
        #if os(Android)
        guard
            // Access current environment
            let env = JEnv.current(),
            // It finds the `java.util.Date` class and loads it directly or from the cache
            let clazz = JClass.load(Self.className),
            // Call to create a new instance of `java.util.Date` and get a global reference to it
            let global = clazz.newObject(env)
        else { return nil }
        // Store the object to access it from methods
        self.object = global
        #else
        // For non-Android platforms, return nil
        return nil
        #endif
    }

    /// Allocates a `Date` object and initializes it to represent the specified number of milliseconds since the standard base time known as "the epoch", namely January 1, 1970, 00:00:00 GMT.
    /// 
    /// - Parameter milliseconds: The number of milliseconds since January 1, 1970, 00:00:00 GMT.
    public init? (_ milliseconds: Int64) {
        #if os(Android)
        guard
            // Access current environment
            let env = JEnv.current(),
            // It finds the `java.util.Date` class and loads it directly or from the cache
            let clazz = JClass.load(Self.className),
            // Call to create a new instance of `java.util.Date`
            // with `milliseconds` parameter and get a global reference to it
            let global = clazz.newObject(env, args: milliseconds)
        else { return nil }
        // Store the object to access it from methods
        self.object = global
        #else
        // For non-Android platforms, return nil
        return nil
        #endif
    }
}

以上就是讓這個類正常運作所需的最低要求。它允許你從頭初始化一個java.util.Date類,或包裝一個已經是正確類別的JObject物件。

好了,骨架已經搭建好了。現在我們需要為它加入一些肌肉,讓我們寫下類別方法!

/// Returns the day of the week represented by this date.
public func day() -> Int32? {
    // Convenience call to `java.util.Date.getDay()`
    object.callIntMethod(name: "getDay")
}

你明白了!現在,繼續對getHoursgetMinutesgetSecondsgetTime方法執行完全相同的操作。它們只是更多相同的模式!

現在來看看更有趣的東西:一個更複雜的方法,它採用另一個JDate作為參數。

/// Tests if this date is before the specified date.
public func before(_ date: JDate) -> Bool {
    // Convenience call to `java.util.Date.before(Date date)`
    // which passes another `Date` object as a parameter
    // and returns a boolean result
    object.callBoolMethod(name: "before", args: date.object.signed(as: JDate.className)) ?? false
}

你猜對了,對after方法也做同樣的操作。它實際上和before完全相同。

最後,為了覆寫絕對最小值並使此類真正有用,讓我們加入一個超級方便的方法,將我們的 Java JDate轉換為本機 Swift Date物件。

/// Converts this java `Date` object to a Swift `Date`.
public func date() -> Date? {
    // Get milliseconds since epoch using `getTime` method
    guard let time = time() else { return nil }
    // Convert milliseconds to seconds and create a Swift `Date` object
    return Date(timeIntervalSince1970: TimeInterval(time) / 1000.0)
}

現在你已經對 Swift 如何透過 JNI 與 Java/Kotlin 協同工作有了基本的了解!希望你已經在你的 Android 專案中成功編譯並測試了它。

各位,今天就到這裡啦!

如需了解更多深入資訊和進階功能,請查看GitHub 上全面的 JNIKit README文件。它包含大量詳細資訊!

在 Swift Stream Discord 社群中找到我,加入並隨時提問!

點擊訂閱,就不會錯過下一篇文章!我們一定會討論如何透過 JitPack 分發庫,深入探討更複雜的 JNI 用例,以及…UI!

敬請關注!


原文出處:https://dev.to/swiftstream/the-swift-android-setup-i-always-wanted-285d


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝10   💬6   ❤️11
459
🥈
我愛JS
📝1   💬5   ❤️4
89
🥉
AppleLily
📝1   💬4   ❤️1
47
#4
💬2  
6
#5
💬1  
5
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次