先來看一個最基本的 K/N 作用于 JNI 的函數(shù)讶泰,它將是一切的開端:
@CName("Java_com_rarnu_common_HelloJni_hello")
fun jniHello(env: CPointer<JNIEnvVar>, thiz: jobject): jstring = memScoped {
return env.pointed.pointed!!.NewStringUTF!!.invoke(env, "Hello NDK".cstr.ptr)!!
}
你是不是會覺得寫這樣的代碼很麻煩,一點都不 Kotlin拂到,甚至還有一些反感痪署?
如果不爽就對了,如果爽的話也就沒有這篇了兄旬,為了把代碼寫舒服了狼犯,真的也是要付出不少代價的,至少在 K/N 的場景下领铐,沒有一些舒服的封裝真的會讓人生不如死的悯森。
那么開始正文,首先想要封裝的東西是文件操作的 API绪撵,由于之前基本上都在用 JVM 下的 Kotlin瓢姻,遇到文件操作基本上就直接 File
了,可惜 K/N 下沒有這東西音诈,K/N 能用的東西基本上就是 Kotlin 標準庫以及 cinterop
雹舀,雖說很強大唆迁,但是真的用起來卻是實實在在的麻煩治专,比如讀取一個文本文件:
actual fun readText() = memScoped {
val st = alloc<stat>()
stat(innerFilePath, st.ptr)
val size = st.st_size
val buf = allocArray<ByteVar>(size)
val f = fopen(innerFilePath, "rb")
fread(buf, 1UL, size.toULong(), f)
fclose(f)
buf.toKString()
}
誒沼撕,這么一寫不就是 C 么?Kotlin 自己沒有 API喇聊?是的恍风,目前就是沒有,所以才如此強調(diào) cinterop
誓篱,所幸的是 cinterop
轉(zhuǎn)換的函數(shù)與 Kotlin 相容性很好朋贬,而且也忠于原始的 C 庫。
這里有一個關(guān)鍵的關(guān)型窜骄,即 stat
锦募,它的定義是這樣的:
@kotlinx.cinterop.internal.CStruct
public final class stat public constructor(rawPtr: kotlinx.cinterop.NativePtr ) : kotlinx.cinterop.CStructVar
或許一開始接觸的人都會很郁悶,構(gòu)造函數(shù)里那個 NativePtr
參數(shù)是啥啊研,要怎么傳參構(gòu)造呢御滩?在這里我必須告訴各位的是,以后看到這種構(gòu)造方式的類型党远,直接在 memScoped
里面 alloc
就好削解,可以直接得到想要的對象,至于利用構(gòu)造函數(shù)來構(gòu)造沟娱,放棄這個想法吧氛驮。相同的情況也出現(xiàn)在構(gòu)造 jni
調(diào)用 java 的傳參問題上,后面會講到济似。
在 K/N 里面矫废,JetBrains 還算照顧我們,提供了一些不錯的轉(zhuǎn)換函數(shù)砰蠢,比如說以下這些蓖扑,可以讓開發(fā)變得更簡單:
toKString() // 將一個CPointer<ByteVar> 轉(zhuǎn)換成 Kotlin 字符串
readBytes() // 將一個 CPointer<ByteVar> 轉(zhuǎn)換成 Kotlin 的 ByteArray
cstr // 將一個 Kotlin 字符串轉(zhuǎn)換成 CValues<ByteVar>
toCValues() // 將一個 Kotlin ByteArray 轉(zhuǎn)換成 CValues<ByteVar>
所以我們可以在此基礎(chǔ)之上,輕松的寫出以下函數(shù):
actual fun readContent() = memScoped {
val st = alloc<stat>()
stat(innerFilePath, st.ptr)
val size = st.st_size
val buf = allocArray<ByteVar>(size)
val f = fopen(innerFilePath, "rb")
fread(buf, 1UL, size.toULong(), f)
fclose(f)
buf.readBytes(size.toInt())
}
actual fun writeContent(content: ByteArray) = memScoped {
val f = fopen(innerFilePath, "wb")
val buf = content.toCValues()
val ret = fwrite(buf, buf.size.toULong(), 1, f)
fclose(f)
ret == 0UL
}
actual fun list(): List<String> = memScoped {
val list = mutableListOf<String>()
val d = opendir(innerFilePath)
while (true) {
val entry = readdir(d)
if (entry == NULL) break
val dname = entry!!.pointed.d_name.toKString()
if (dname == "." || dname == ".." || dname.trim() == "") continue
list.add(dname)
}
return list
}
同樣的思路台舱,可以把常用的文件操作都包裝起來律杠,就像 JVM 下的 File
一樣,在這里(點擊查看)我放了一份封裝后的文件竞惋,可以直接取用柜去。
下面要來封裝一下 JNI 相關(guān)的函數(shù)了,像開頭那種寫法太不友好了拆宛,有必要造橋嗓奢。橋的造法各有千秋,這里我不打算對各種方法作任何的評論浑厚,只談自己的封裝方法股耽。
首先我一定會把 env: CPointer<JNIEnvVar>
這個對象封裝掉,在 JNI 方法中钳幅,時時刻刻要用它豺谈,于是先寫個簡單架子,把 env
和一些常用函數(shù)寫進去:
data class JniClass(val jclass: jclass)
data class JniObject(val jobject: jobject)
data class JniMethod(val jmethod: jmethodID)
fun asJniClass(jclass: jclass?) = if (jclass != null) JniClass(jclass) else null
fun asJniObject(jobject: jobject?) = if (jobject != null) JniObject(jobject) else null
fun asJniMethod(jmethodID: jmethodID?) = if (jmethodID != null) JniMethod(jmethodID) else null
class JniBridge(val env: CPointer<JNIEnvVar>) {
private val innerEnv = env.pointed.pointed!!
private val fNewStringUTF = innerEnv.NewStringUTF!!
private val fGetStringUTFChars = innerEnv.GetStringUTFChars!!
private val fReleaseStringUTFChars = innerEnv.ReleaseStringUTFChars!!
... ...
}
基本的架子就這么簡單贡这,后面慢慢加內(nèi)容茬末,有了這些東西后,可以寫兩個基本函數(shù)盖矫,用于完成 String
和 jstring
的相互轉(zhuǎn)換:
class JniBridge(val env: CPointer<JNIEnvVar>) {
... ...
private fun toJString(string: String) = memScoped {
val result = asJniObject(fNewStringUTF(env, string.cstr.ptr))
check()
result
}
private fun toKString(string: jstring) = memScoped {
val isCopy = alloc<jbooleanVar>()
val chars = fGetStringUTFChars(env, string, isCopy.ptr)
var ret: String? = null
if (chars != null) {
ret = chars.toKString()
fReleaseStringUTFChars(env, string, chars)
}
ret
}
}
這次再次遇到 alloc<jbooleanVar>
這種寫法丽惭,同樣的,它也是一個接受 NativePtr
作為構(gòu)造參數(shù)的類型辈双,可以直接 alloc
责掏。
寫完后要怎么用呢?方法如下:
val jniStr = JniBridge(env).toJString("hello")
此時就能得到一個 jstring
類型的對象了湃望。但是對于我來說换衬,我覺得它依然不方便痰驱,我希望可以在字符串對象上直接轉(zhuǎn)換,那么再擴展下:
class JniBridge(val env: CPointer<JNIEnvVar>) {
... ...
fun String.asJString() = toJString(this)!!.jobject
fun jstring.asKString() = toKString(this)
}
這下可好瞳浦,是不是更加不知道怎么用了担映?因為 String
或 jstring
的上下文都沒有嘛,現(xiàn)在就要開始變魔術(shù)了叫潦,我們在 JniBridge
里再加一個方法:
class JniBridge(val env: CPointer<JNIEnvVar>) {
... ...
val fPushLocalFrame = innerEnv.PushLocalFrame!!
val fPopLocalFrame = innerEnv.PopLocalFrame!!
... ...
inline fun <T> withLocalFrame(block: JniBridge.() -> T): T {
if (fPushLocalFrame(env, 0) < 0) throw Error("Cannot push new local frame")
try { return block() } finally { fPopLocalFrame(env, null) }
}
}
有了這個函數(shù)后蝇完,我們可以在全局加一個函數(shù),來實現(xiàn)對完整上下文的包裝:
inline fun <T> jniWith(env: CPointer<JNIEnvVar>, block: JniBridge.() -> T) =
JniBridge(env).withLocalFrame(block)
下面就是使用了矗蕊,我們現(xiàn)在已經(jīng)完成了變魔術(shù)所需要的條件短蜕,把本文開頭的那個函數(shù)改一下:
@CName("Java_com_rarnu_common_HelloJni_hello")
fun jniHello(env: CPointer<JNIEnvVar>, thiz: jobject): jstring = jniWith(env) {
"Hello NDK".asJString()
}
這么一來,就看不到 JNI 函數(shù)在背后的動作了傻咖,API 非常簡潔朋魔,對開發(fā)者友好。我們可以把所有的操作都寫在 jniWith
里面卿操,它具備 JniBridge
的完整上下文铺厨。
下面再來看一下如何從 JNI 調(diào)用 Java 方法,有了上面的封裝經(jīng)驗后硬纤,要搞個好玩的東西出來就簡單了:
class JniBridge(val env: CPointer<JNIEnvVar>) {
... ...
private val fFindClass = innerEnv.FindClass!!
private val fGetMethodID = innerEnv.GetMethodID!!
private val fCallObjectMethodA = innerEnv.CallObjectMethodA!!
private val fGetObjectClass = innerEnv.GetObjectClass!!
... ...
fun findClass(name: String) = memScoped {
asJniClass(fFindClass(env, name.cstr.ptr))
}
fun getObjectClass(obj: jobject) = memScoped {
asJniClass(fGetObjectClass(env, obj))
}
fun getMethodID(clazz: JniClass?, name: String, signature: String) = memScoped {
asJniMethod(fGetMethodID(env, clazz?.jclass, name.cstr.ptr, signature.cstr.ptr))
}
fun callObjectMethod(receiver: JniObject?, method: JniMethod, vararg arguments: Any?) = memScoped {
asJniObject(fCallObjectMethodA(env, receiver?.jobject, method.jmethod, null))
}
}
細心的話你會發(fā)現(xiàn)這里留了個尾巴解滓,調(diào)用函數(shù)如何傳參呢?雖然寫了 arguments
參數(shù)筝家,但是實際傳參是 null
洼裤,很顯然這里需要把參數(shù)補齊。
這個參數(shù)是一個 CPointer<jvalue>
類型的對象溪王,因此我們就必須把參數(shù)構(gòu)造成這樣的腮鞍,才可以正常傳遞,在此又要多寫一個函數(shù):
class JniBridge(val env: CPointer<JNIEnvVar>) {
... ...
private fun toJValues(arguments: Array<out Any?>, scope: MemScope): CPointer<jvalue>? {
val result = scope.allocArray<jvalue>(arguments.size)
arguments.mapIndexed { index, it -> when (it) {
null -> result[index].l = null
is JniObject -> result[index].l = it.jobject
is String -> result[index].l = toJString(it)?.jobject
is Int -> result[index].i = it
is Long -> result[index].j = it
is Byte -> result[index].b = it
is Short -> result[index].s = it
is Double -> result[index].d = it
is Float -> result[index].f = it
is Char -> result[index].c = it.toInt().toUShort()
is Boolean -> result[index].z = (if (it) JNI_TRUE else JNI_FALSE).toUByte()
else -> throw Error("Unsupported conversion for ${it::class.simpleName}")
}}
return result
}
}
看起來很復(fù)雜莹菱,但是實質(zhì)上是在根據(jù)不同的參數(shù)類型移国,對 jvalue
進行填充,這里再一次的用到了 alloc
道伟,來對一組 jvalue
進行初始化迹缀,這是必須掌握的一種寫法,要好好記住哦:)
這里還有一個 scope: MemScope
參數(shù)蜜徽,這是個什么東西呢祝懂?其實它來源于 memScoped
方法,會直接構(gòu)造出一個 MemScope
類型拘鞋,toJValues
必須用這種傳入 scope 的方法來實現(xiàn)砚蓬,是因為 memScoped
會在函數(shù)結(jié)束時,回收分配的內(nèi)存盆色,而我們構(gòu)造出來的 CPointer<jvalue>
卻必須被返回灰蛙,并且被使用后才可以銷毀祟剔,因此對它的內(nèi)存管理必須依賴上一個 scope。
好了摩梧,有了這個方法后物延,改一下上面的代碼:
fun callObjectMethod(receiver: JniObject?, method: JniMethod, vararg arguments: Any?) = memScoped {
asJniObject(fCallObjectMethodA(env, receiver?.jobject, method.jmethod, toJValues(arguments, this@memScoped)))
}
不過我依然覺得這樣不簡潔,我希望有更簡單的寫法障本,加一個擴展:
class JniBridge(val env: CPointer<JNIEnvVar>) {
... ...
fun Array<*>.asJValues(scope: MemScope) = toJValues(this, scope)
}
這樣就又可以做一個很細小的改動了:
fun callObjectMethod(receiver: JniObject?, method: JniMethod, arguments: Array<Any>?) = memScoped {
asJniObject(fCallObjectMethodA(env, receiver?.jobject, method.jmethod, arguments?.asJValues(this@memScoped)))
}
最終我們想要的效果是這樣的:
@CName("Java_com_rarnu_common_HelloJni_callJvm")
fun jniCallJvm(env: CPointer<JNIEnvVar>, thiz: jobject): jstring = jniWith(env) {
val jcls = getObjectClass(thiz)
val jmthd = getMethodID(jcls, "callFromNative", "(ILjava/lang/String;)Ljava/lang/String;")
callObjectMethod(thiz, jmthd!!, 1, "NDK")!!.jobject
}
以上對于 JNI 的封裝,我同樣提供了一個完整文件供取用响鹃,點擊查閱
另外驾霜,我也發(fā)布了一個 K/N for NDK 的封裝庫,如果你打算使用 Kotlin 來開發(fā) NDK 應(yīng)用买置,可以直接在 gradle 內(nèi)使用它:
... ...
maven { url 'http://119.3.22.119:8081/repository/maven-releases' }
... ...
implementation 'com.rarnu:kn-common-ndk:0.0.1' // for common
... ...
implementation 'com.rarnu:kn-common-ndk-android64:0.0.1' // for android64