设备与系统信息
源:Android Build | iOS UIDevice
获取设备信息是应用的基础需求,用于日志追踪、兼容性判断、用户分析等场景。本文将展示如何跨平台获取设备型号、系统版本、唯一标识等信息。
平台差异对比
| 平台 | 原生 API | 可获取信息 | 隐私限制 |
|---|---|---|---|
| Android | Build + PackageManager | 设备型号、系统版本、CPU 架构、ANDROID_ID | Android 10+ 限制设备 ID |
| iOS/macOS | UIDevice + ProcessInfo | 设备型号、系统版本、IDFV | 严格限制设备标识符 |
| Desktop (JVM) | System.getProperty() | OS 名称、版本、CPU 架构 | 无限制 |
| Desktop (Native) | Platform API | 系统信息、硬件信息 | 依赖平台 |
expect/actual 实现方案
API 核心签名说明
expect class DeviceInfoval DeviceInfo.platform: Platformval DeviceInfo.osVersion: Stringval DeviceInfo.deviceModel: Stringval DeviceInfo.deviceId: Stringval DeviceInfo.appVersion: String
标准代码块
kotlin
enum class Platform {
ANDROID, IOS, DESKTOP
}
expect class DeviceInfo {
val platform: Platform
val platformName: String
val osVersion: String
val deviceModel: String
val deviceManufacturer: String
val deviceId: String
val appVersion: String
val appBuildNumber: String
}
// 业务层使用
class AnalyticsService(private val deviceInfo: DeviceInfo) {
fun logAppStart() {
println("App started on ${deviceInfo.platformName}")
println("Device: ${deviceInfo.deviceModel}")
println("OS: ${deviceInfo.osVersion}")
println("Version: ${deviceInfo.appVersion}")
}
fun buildUserAgent(): String {
return "${deviceInfo.platformName}/${deviceInfo.osVersion} " +
"(${deviceInfo.deviceModel}) " +
"App/${deviceInfo.appVersion}"
}
}kotlin
import android.content.Context
import android.os.Build
import android.provider.Settings
actual class DeviceInfo(private val context: Context) {
actual val platform: Platform = Platform.ANDROID
actual val platformName: String = "Android"
actual val osVersion: String = "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
actual val deviceModel: String = "${Build.MANUFACTURER} ${Build.MODEL}"
actual val deviceManufacturer: String = Build.MANUFACTURER
actual val deviceId: String by lazy {
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "unknown"
}
actual val appVersion: String by lazy {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
packageInfo.versionName ?: "unknown"
} catch (e: Exception) {
"unknown"
}
}
actual val appBuildNumber: String by lazy {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode.toString()
} else {
@Suppress("DEPRECATION")
packageInfo.versionCode.toString()
}
} catch (e: Exception) {
"unknown"
}
}
// 扩展:获取 CPU 架构
val cpuArchitecture: String = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"
// 扩展:是否为平板
val isTablet: Boolean by lazy {
val configuration = context.resources.configuration
val screenLayout = configuration.screenLayout and
android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK
screenLayout >= android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
}
}kotlin
import platform.Foundation.*
import platform.UIKit.UIDevice
actual class DeviceInfo {
actual val platform: Platform = Platform.IOS
actual val platformName: String = UIDevice.currentDevice.systemName
actual val osVersion: String = UIDevice.currentDevice.systemVersion
actual val deviceModel: String = UIDevice.currentDevice.model
actual val deviceManufacturer: String = "Apple"
actual val deviceId: String by lazy {
UIDevice.currentDevice.identifierForVendor?.UUIDString ?: "unknown"
}
actual val appVersion: String by lazy {
NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String ?: "unknown"
}
actual val appBuildNumber: String by lazy {
NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleVersion") as? String ?: "unknown"
}
// 扩展:获取设备具体型号(如 iPhone 14 Pro)
val deviceModelDetailed: String by lazy {
var sysinfo = utsname()
uname(&sysinfo)
val modelCode = NSString.stringWithCString(
CPointer.narrow(sysinfo.machine.ptr),
NSUTF8StringEncoding
) ?: "unknown"
mapDeviceModel(modelCode as String)
}
private fun mapDeviceModel(code: String): String {
return when {
code.contains("iPhone15,2") -> "iPhone 14 Pro"
code.contains("iPhone15,3") -> "iPhone 14 Pro Max"
code.contains("iPhone14") -> "iPhone 13"
code.contains("iPad13") -> "iPad Pro"
code.contains("x86_64") || code.contains("i386") -> "Simulator"
else -> code
}
}
}kotlin
actual class DeviceInfo {
actual val platform: Platform = Platform.DESKTOP
actual val platformName: String = System.getProperty("os.name") ?: "Unknown"
actual val osVersion: String = System.getProperty("os.version") ?: "Unknown"
actual val deviceModel: String = buildString {
append(System.getProperty("os.name"))
append(" ")
append(System.getProperty("os.arch"))
}
actual val deviceManufacturer: String = "Generic"
actual val deviceId: String by lazy {
// 使用 MAC 地址或主机名作为设备标识
try {
java.net.InetAddress.getLocalHost().hostName
} catch (e: Exception) {
"desktop-${System.currentTimeMillis()}"
}
}
actual val appVersion: String = "1.0.0" // 可从 build.gradle 读取
actual val appBuildNumber: String = "1"
// 扩展:JVM 信息
val javaVersion: String = System.getProperty("java.version") ?: "Unknown"
val javaVendor: String = System.getProperty("java.vendor") ?: "Unknown"
}代码封装示例
以下是带缓存和格式化的完整封装:
kotlin
// commonMain
data class DeviceEnvironment(
val platform: Platform,
val osName: String,
val osVersion: String,
val deviceModel: String,
val appVersion: String,
val deviceId: String,
val locale: String
) {
fun toUserAgent(): String {
return "$osName/$osVersion ($deviceModel) App/$appVersion"
}
fun toAnalyticsMap(): Map<String, String> {
return mapOf(
"platform" to platform.name,
"os_name" to osName,
"os_version" to osVersion,
"device_model" to deviceModel,
"app_version" to appVersion,
"device_id" to deviceId,
"locale" to locale
)
}
}
class DeviceEnvironmentProvider(private val deviceInfo: DeviceInfo) {
private var cachedEnvironment: DeviceEnvironment? = null
fun getEnvironment(): DeviceEnvironment {
return cachedEnvironment ?: DeviceEnvironment(
platform = deviceInfo.platform,
osName = deviceInfo.platformName,
osVersion = deviceInfo.osVersion,
deviceModel = deviceInfo.deviceModel,
appVersion = deviceInfo.appVersion,
deviceId = deviceInfo.deviceId,
locale = getCurrentLocale()
).also { cachedEnvironment = it }
}
private fun getCurrentLocale(): String {
// 由各平台实现
return "en_US"
}
}第三方库推荐
KMP 设备信息库对比
| 库名 | 维护状态 | 功能完整性 | 推荐度 |
|---|---|---|---|
| 自实现 | - | 基础功能 | ⭐⭐⭐⭐ |
| Napier | 活跃 | 日志+设备信息 | ⭐⭐⭐ |
| Kermit | 活跃 | 日志+设备信息 | ⭐⭐⭐ |
推荐方案
设备信息的获取逻辑相对简单,建议自实现以保持轻量级。若项目已集成日志库,可复用其设备信息采集模块。
依赖补充
自实现无需额外依赖
设备信息获取仅依赖平台标准库,无需添加第三方依赖。
Android 扩展权限(可选)
如需获取更多设备信息(如 IMEI),需在 AndroidManifest.xml 添加权限:
xml
<!-- 仅在必要时使用,Android 10+ 普通应用无法获取 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />实战坑点
Android ANDROID_ID 不稳定
设备 ID 重置
ANDROID_ID 在以下情况会改变:
- 应用卸载后重装
- 设备恢复出厂设置
- 应用签名变更(Android 8.0+)
解决方案:
生成 UUID 并持久化到本地存储作为稳定设备标识:
kotlin
class StableDeviceId(private val settings: Settings) {
fun getDeviceId(): String {
var id = settings.getString("stable_device_id", "")
if (id.isEmpty()) {
id = UUID.randomUUID().toString()
settings.putString("stable_device_id", id)
}
return id
}
}iOS identifierForVendor 为 nil
IDFV 获取失败
在极少数情况下(如设备未正确初始化),identifierForVendor 可能返回 nil。
解决方案:
提供降级方案:
kotlin
actual val deviceId: String by lazy {
UIDevice.currentDevice.identifierForVendor?.UUIDString
?: generateFallbackId()
}
private fun generateFallbackId(): String {
val uuid = NSUUID.UUID().UUIDString
// 可选:持久化到 UserDefaults
return uuid
}Desktop 主机名获取异常
网络未配置
在某些 Desktop 环境(如 Docker 容器),InetAddress.getLocalHost() 可能抛出异常。
解决方案:
kotlin
actual val deviceId: String by lazy {
try {
java.net.InetAddress.getLocalHost().hostName
} catch (e: Exception) {
// 降级为 MAC 地址或生成 UUID
generateMacBasedId() ?: UUID.randomUUID().toString()
}
}版本号格式不一致
::: caution 跨平台版本号差异 Android versionName 可以是任意字符串(如 "v1.2.3-beta"),iOS 必须符合语义化版本。 :::
统一方案:
在 commonMain 定义版本号解析器:
kotlin
data class AppVersion(
val major: Int,
val minor: Int,
val patch: Int,
val suffix: String = ""
) {
companion object {
fun parse(versionString: String): AppVersion {
val regex = Regex("""(\d+)\.(\d+)\.(\d+)(.*)""")
val match = regex.matchEntire(versionString) ?: return AppVersion(0, 0, 0)
return AppVersion(
major = match.groupValues[1].toInt(),
minor = match.groupValues[2].toInt(),
patch = match.groupValues[3].toInt(),
suffix = match.groupValues[4]
)
}
}
}隐私合规风险
GDPR 与设备标识符
在欧盟地区,设备标识符属于个人数据,必须:
- 在隐私政策中披露
- 获得用户同意
- 提供删除/重置机制
合规建议:
kotlin
class ConsentManager(private val settings: Settings) {
fun hasUserConsent(): Boolean {
return settings.getBoolean("analytics_consent", false)
}
fun requestConsent() {
// 显示同意对话框
}
fun getDeviceIdIfConsented(deviceInfo: DeviceInfo): String? {
return if (hasUserConsent()) {
deviceInfo.deviceId
} else {
null
}
}
}