返回

iOS 模拟器设备类型检测:准确获取 iPhone/iPad 型号

IOS

如何检测 iOS 模拟器正在运行的设备类型?

经常遇到需要根据不同的设备类型执行不同逻辑的情况,真机上,获取设备信息很简单,但在模拟器上如何操作呢?很多资料都是关于判断是否在模拟器上运行,而不是获取模拟器的具体设备型号。 这篇文章就来说说怎么在 iOS 模拟器上获取“我正在模拟 iPhone 15 Pro Max(举个例子)”这样的信息。

一、 问题的根源

iOS 模拟器本身的设计目的就是尽可能地模拟真实的 iOS 设备环境,它拥有和真实设备几乎相同的 API 行为。 但为了性能等考虑,直接查询具体的模拟设备名称并没有提供直接的,方便使用的API.

我们之所以能获取信息,核心在于利用系统已有的,通过不同的条件和方法获取。

二、解决方案

下面提供几种不同的方案,获取模拟器所模拟的设备类型。

1. 通过 sysctlbyname 函数获取硬件信息

sysctlbyname 是一个强大的底层函数,它可以获取系统内核的各种信息,其中就包括硬件型号。

  • 原理: sysctlbyname 通过传入一个字符串形式的 "Management Information Base" (MIB) 名称来查询系统信息。对于硬件型号,我们可以使用 "hw.machine"
  • 代码示例:
import Foundation

func getSimulatorModel() -> String? {
    var size = 0
    sysctlbyname("hw.machine", nil, &size, nil, 0)
    var machine = [CChar](repeating: 0,  count: size)
    sysctlbyname("hw.machine", &machine, &size, nil, 0)
    return String(cString: machine)
}

//使用示例
if let model = getSimulatorModel() {
   print("模拟器硬件型号:\(model)") //例如:x86_64, arm64等

   // 可以进行额外的转换, 但是不能100%保证, 参考下面的"进阶技巧".
} else {
    print("无法获取模拟器型号。")
}
  • 安全建议: sysctlbyname 本身是安全的,因为它只用于读取系统信息,不涉及修改操作。
  • 进阶技巧: 虽然通过"hw.machine" 我们可以知道是不是模拟器,例如: x86_64, arm64, 但是单凭这个我们是无法知道,例如具体是 "iPhone 15 Pro Max" ,还是 "iPhone 14 Pro", 我们还需要额外信息辅助. 可以看下面的解决方案,继续处理.

2. 通过 UIDevice 获取信息(不完全可靠)

UIDevice 类提供了 modelname 属性,但这两个属性在模拟器上的行为需要特别注意。

  • 原理: UIDevice.current.model 在真机上会返回类似 "iPhone"、"iPad" 这样的值,但在模拟器上通常返回 "iPhone" 或者 "iPad",无法提供更详细的信息。UIDevice.current.name 属性, 用户可能自定义设置.
  • 代码示例:
import UIKit

let model = UIDevice.current.model
let name = UIDevice.current.name
print("设备模型:\(model)") // 通常是 iPhone 或 iPad
print("设备名称:\(name)")   //用户可能修改
  • 说明 : 由于 UIDevice 的这些属性提供的信息比较有限,并且name 属性, 用户是可以自定义修改的, 通常需要配合方案1中的 sysctlbyname 返回结果使用, 不可靠。

3. 读取模拟器的 plist 文件 (最可靠,推荐)

每个 iOS 模拟器都有一个对应的 plist 文件,其中包含了模拟器的详细配置信息,包括设备类型。这个方法比较hack, 但却比较实用.

  • 原理: 通过读取这个 plist 文件,可以精确地获取模拟器的设备类型。模拟器的配置文件通常位于 ~/Library/Developer/CoreSimulator/Devices/<Simulator_UUID>/device.plist。我们可以通过SIMCTL_CHILD_UDID环境变量先获取 UUID.
  • 代码示例:
import Foundation

func getSimulatorDeviceType() -> String? {
    guard let simulatorUDID = ProcessInfo.processInfo.environment["SIMCTL_CHILD_UDID"] else {
          return nil // 不是模拟器环境, 或者无法读取环境变量
      }
    
    let plistPath =  "\(NSHomeDirectory())/Library/Developer/CoreSimulator/Devices/\(simulatorUDID)/device.plist"

    guard let plistData = try? Data(contentsOf: URL(fileURLWithPath: plistPath)),
          let plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any],
          let deviceType = plist["deviceType"] as? String else {
          return nil
      }

      // 对 deviceType 进行格式化
        let device = deviceType
          .replacingOccurrences(of: "com.apple.CoreSimulator.SimDeviceType.", with: "")
          .replacingOccurrences(of: "-", with: " ")

        return device
}
// 使用
let simDeviceType = getSimulatorDeviceType()

if let device = simDeviceType {
        print("模拟的设备:\(device)")//例如:iPhone 15 Pro Max
}else{
    print("未获取到模拟器型号")
}
  • 安全建议: 该方法只是读取一个 plist 文件,不存在任何风险.
  • 原理详细解释: SIMCTL_CHILD_UDID环境变量, 是模拟器进程独有的环境变量,通过这个UUID我们就可以精确的定位到对应模拟器的配置信息,该信息包含设备类型标识符 (deviceType), 例如: "com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro-Max", 对字符串做简单处理就能获取易读的 "iPhone 15 Pro Max".

4. 综合判断

在实际应用中,可以结合上述方法进行判断,以提高准确性。 比如,我们可以先判断是不是模拟器, 然后尝试方案3读取设备型号.如果都获取不到再处理特殊逻辑.


import Foundation
import UIKit

func getDeviceModel() -> String {

    // 方案 3:读取 plist 文件
    if let simulatorDeviceType = getSimulatorDeviceType() {
        return simulatorDeviceType
    }

    // 方案 1:通过 sysctlbyname 获取硬件信息(作为备选)
    if let simulatorModel = getSimulatorModel() {
      //根据模拟器进行大致的映射关系,可能会过时或者错误,应优先选择方法3
      if simulatorModel == "x86_64" || simulatorModel == "arm64"{
           if UIDevice.current.model.contains("iPhone") {
              return "Generic iPhone Simulator" //兜底显示,不具体显示型号.
           }else if UIDevice.current.model.contains("iPad"){
              return "Generic iPad Simulator" //兜底显示,不具体显示型号.
           }else{
              return "Generic Simulator"  //兜底显示,不具体显示型号.
           }
       }
    }

   //如果不是模拟器,尝试获取真机设备型号, 此代码省略,请自行搜索实现。
   // ... ... 真机获取...
   return "Unknown Device"
}

let finalDeviceType = getDeviceModel()
print("最终确定的设备型号: \(finalDeviceType)")

代码首先尝试最精准的方法3 (通过plist). 若方法3无法获取信息(可能是在非模拟器环境下运行),再降级使用 sysctlbyname, 但是只是判断出Generic iPhone/iPad/Simulator模拟器类型. 若最终也无法获取, 才显示 Unknown Device.
通过这个策略, 实现代码的最大兼容性。

三、总结

获取 iOS 模拟器设备类型的方法有多种,各有优劣。建议优先使用方法3,读取模拟器的 plist 文件,获取准确信息。
对于其他方案,都有不小的局限性。结合使用各种方法以提高准确性,这才是比较稳妥的选择。