需求 & 尝试#
今天在折腾 Kotlin Multiplatform 遇到了一个简单的需求,获取 iOS 设备型号。
查找 Apple 官方文档,找到了 UIKit.UIDevice.
这里的 model 属性的介绍如下:
Possible examples of model strings are ”iPhone” and ”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 在互调用这方面上心点