需求與嘗試#
今天在折騰 Kotlin Multiplatform 遇到了一個簡單的需求,獲取 iOS 設備型號。
查找 Apple 官方文件,找到了 UIKit.UIDevice。
這裡的 model 屬性的介紹如下:
可能的型號字符串示例為 “iPhone” 和 “iPod touch”。
很明顯並不符合獲取具體型號的要求,於是繼續 Google,在 strackoverflow 找到了 這篇回答,其中高贊回答中,大多使用到了 utsname 或 sysctlbyname,其實兩種方式具體實現是一樣的,一個簡單一個複雜的區別。
使用 Kotlin Native 複刻#
方法一(簡單)#
首先來看 Objective-C 的實現,非常簡單,聲明出 systemInfo
再調用 uname
傳入 systemInfo
的指針,最後獲取 systemInfo.machine
的內容轉為 NSString。
#import <sys/utsname.h> // import it in your header or implementation file.
NSString* deviceName()
struct utsname systemInfo;
uname(&systemInfo);
return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
}
那我們就可以很方便的轉換為 Kotlin Native 的寫法,首先聲明一個 memScoped,因為類似於 alloc
這樣的內存操作必須要在 memScoped 裡完成:
@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo() {
return memScoped {
// todo
}
}
使用 alloc<T>
聲明一個類型為 utsname 的變量,然後將指針傳入 uname:
@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo() {
return memScoped {
val systemInfo = alloc<utsname>()
uname(systemInfo.ptr)
}
}
接下來發現 systemInfo.machine 的類型是 CArrayPointer<ByteVar>
,也就是 char *
,那麼怎麼轉換為 Kotlin String 呢?Kotlin Native 提供了這樣一個擴展函數:
fun CPointer<ByteVar>.toKString(): String
從字面上就能看出,是用來把 CPointer 轉為 Kotlin 字串的。
由於 CArrayPointer<T> = CPointer<T>
,我們可以直接使用這個擴展函數來實現 char *
到 String
的轉化,最終代碼如下:
@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo(): String {
return memScoped {
val systemInfo = alloc<utsname>()
uname(systemInfo.ptr)
systemInfo.machine.toKString()
}
}
方法二(麻煩)#
第二種方法對應的 Objective-C 實現長這樣:
+ (NSString *)getModel {
size_t size;
sysctlbyname("hw.machine", NULL, &size, NULL, 0);
char *model = malloc(size);
sysctlbyname("hw.machine", model, &size, NULL, 0);
NSString *deviceModel = [NSString stringWithCString:model encoding:NSUTF8StringEncoding];
free(model);
return deviceModel;
}
簡單分析,不難發現,這裡調用 sysctlbyname 後拿到了 char size,然後再調用一次 sysctlbyname 拿到了 hw.machine
的值,最後通過 stringWithCString 轉為了 NSString。
按照慣例,先聲明一個 memScope:
@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo() {
return memScoped {
// todo
}
}
按照剛才的寫法,首先實現對第一次 sysctlbyname
的調用:
val sizePtr = alloc<size_tVar>()
sysctlbyname("hw.machine", null, sizePtr.ptr, null, 0u)
非常簡單,跟剛才一樣,接下來我們通過 allocArray
聲明一個定長數組,長度為 sizePtr
,對應上面的 char *model = malloc(size)
:
val model = allocArray<ByteVar>(sizePtr.value.toInt())
接下來直接調用 sysctlbyname
,參考方法一中的方法,調用 sysctlbyname
並將 model
轉換為 Kotlin 字串,於是最終代碼如下:
@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo(): String {
return memScoped {
val sizePtr = alloc<size_tVar>()
sysctlbyname("hw.machine", null, sizePtr.ptr, null, 0u)
val model = allocArray<ByteVar>(sizePtr.value.toInt())
sysctlbyname("hw.machine", model, sizePtr.ptr, null, 0u)
model.toKString()
}
}
有人可能會問, 為什麼我們的代碼裡沒有 free?這就是 memScoped
的作用了,Kotlin Native 會自己管理內存,自動釋放 model,不需要我們關心。
測試#
在 Xcode iOS 模擬器下運行,成功得到了正確的結果(虛擬機為 x86_64,如果實體機會顯示型號代碼):
由於沒有 iOS 設備,這裡沒法進行實體機上的測試(
吐槽#
Kotlin Native 還需要進步,Kotlin Multiplatform 已經 Stable 了,希望 JetBrains 在互調用這方面上心點