前言

	NS_CLASS_AVAILABLE_IOS(2_0) @interface UIDevice : NSObject
@available(iOS 2.0, *) public class UIDevice : NSObject
  • iOS 的 APP 应用开发的过程中,有时为了 bug 跟踪或者获取用反馈的需要自动收集用户设备、系统信息、应用信息等等,这些信息方便开发者诊断问题,当然这些信息是用户的非隐私信息,是通过开发 api 可以获取到的。那么通过那些 api 可以获取这些信息呢,iOS 的 SDK 中提供了 UIDevice,NSBundle,NSLocale。

  • UIDevice 提供了多种属性、类函数及状态通知,帮助我们全方位了解设备状况。从检测电池电量到定位设备与临近感应,UIDevice 所做的工作就是为应用程序提供用户及设备的一些信息。UIDevice 类还能够收集关于设备的各种具体细节,例如机型及 iOS 版本等。其中大部分属性都对开发工作具有积极的辅助作用。

  • bundle 是一个目录,其中包含了程序会使用到的资源。这些资源包含了如图像、声音、编译好的代码、nib 文件(用户也会把 bundle 称为 plug-in),对应 bundle,cocoa 提供了类 NSBundle。一个应用程序看上去和其他文件没有什么区别. 但是实际上它是一个包含了 nib 文件,编译代码,以及其他资源的目录。我们把这个目录叫做程序的 main bundle。通过这个路径可以获取到应用的信息,例如应用名、版本号等。

  • NSLocale 可以获取用户的本地化信息设置,例如货币类型,国家,语言,数字,日期格式的格式化,提供正确的地理位置显示等等。下面的代码获取机器当前语言和国家代码。

1、设备相关信息的获取

  • Objective-C

    • 设备基本信息

      	// 获取当前设备
      UIDevice *device = [UIDevice currentDevice]; // 获取设备名称
      /*
      e.g. "My iPhone"
      */
      NSString *name = device.name; // 获取设备类型
      /*
      e.g. @"iPhone", @"iPod touch"
      */
      NSString *model = device.model; // 获取本地化设备类型
      /*
      localized version of model
      */
      NSString *localizedModel = device.localizedModel; // 获取设备系统名称
      /*
      e.g. @"iOS"
      */
      NSString *systemName = device.systemName; // 获取设备系统版本
      /*
      e.g. @"4.0"
      */
      NSString *systemVersion = device.systemVersion; // 获取设备 UUID
      /*
      可用于唯一标识该设备,同一供应商不同应用具有相同的 UUID
      */
      NSUUID *identifierForVendor = device.identifierForVendor;
      NSString *UUID = identifierForVendor.UUIDString;
    • 设备方向

      	// 判断设备是否生成设备转向通知
      BOOL generatesDeviceOrientationNotifications = device.isGeneratingDeviceOrientationNotifications; // 开启设备转向通知
      /*
      通过调用该方法通知设备:如果用户改变了设备的朝向,我们想获悉这一点
      在注册设备方向通知时,需要先调用该方法
      */
      [device beginGeneratingDeviceOrientationNotifications]; // 停止设备转向通知
      /*
      在移除设备方向通知后,需要调用该方法
      */
      [device endGeneratingDeviceOrientationNotifications]; // 注册屏幕方向变化通知
      [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(deviceOrientationDidChange)
      name:@"UIDeviceOrientationDidChangeNotification"
      object:nil]; // 获取设备方向
      /*
      UIDeviceOrientationUnknown,
      UIDeviceOrientationPortrait, // home button on the bottom 竖向,头向上
      UIDeviceOrientationPortraitUpsideDown, // home button on the top 竖向,头向下
      UIDeviceOrientationLandscapeLeft, // home button on the right 横向,头向左
      UIDeviceOrientationLandscapeRight, // home button on the left 横向,头向右
      UIDeviceOrientationFaceUp, // face up 平放,屏幕朝上
      UIDeviceOrientationFaceDown // face down 平放,屏幕朝下 除非正在生成设备方向的通知,否则返回 UIDeviceOrientationUnknown
      */
      UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
    • 设备电池监控

      	// 判断设备是否开启电池监控
      /*
      default is NO
      */
      BOOL batteryMonitoringEnabled = device.isBatteryMonitoringEnabled; // 开启电池监控
      /*
      default is NO
      */
      device.batteryMonitoringEnabled = YES; // 获取设备电池状态
      /*
      UIDeviceBatteryStateUnknown,
      UIDeviceBatteryStateUnplugged, // on battery, discharging 未充电
      UIDeviceBatteryStateCharging, // plugged in, less than 100% 正在充电
      UIDeviceBatteryStateFull, // plugged in, at 100% 满电 如果禁用电池监控,则电池状态为 UIDeviceBatteryStateUnknown
      */
      UIDeviceBatteryState batteryState = device.batteryState; // 注册电池状态变化通知
      [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(deviceBatteryStateDidChange)
      name:@"UIDeviceBatteryStateDidChangeNotification"
      object:nil]; // 获取电池电量
      /*
      0 .. 1.0. 如果电池状态为 UIDeviceBatteryStateUnknown,百分比为 -1.0
      */
      float batteryLevel = device.batteryLevel; // 注册电池电量变化通知
      [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(deviceBatteryLevelDidChange)
      name:@"UIDeviceBatteryLevelDidChangeNotification"
      object:nil];
    • 设备接近状态监控

      	// 判断设备是否开启接近状态监控
      /*
      default is NO
      */
      BOOL proximityMonitoringEnabled = device.isProximityMonitoringEnabled; // 开启接近状态监控
      /*
      default is NO
      */
      device.proximityMonitoringEnabled = YES; // 获取接近状态
      /*
      如果设备不具备接近感应器,则总是返回 NO
      */
      BOOL proximityState = device.proximityState; // 注册接近状态变化通知 [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(deviceProximityStateDidChange)
      name:@"UIDeviceProximityStateDidChangeNotification"
      object:nil];
    • 设备性能

      	// 判断设备是否支持多任务
      BOOL multitaskingSupported = device.multitaskingSupported;
    • 设备操作

      	// 获取用户界面模式
      /*
      UIUserInterfaceIdiomUnspecified = -1,
      UIUserInterfaceIdiomPhone // iPhone and iPod touch style UI iPhone 和 iPod touch 风格
      UIUserInterfaceIdiomPad // iPad style UI iPad 风格
      UIUserInterfaceIdiomTV // Apple TV style UI Apple TV 风格
      UIUserInterfaceIdiomCarPlay // CarPlay style UI CarPlay 风格
      */
      UIUserInterfaceIdiom userInterfaceIdiom = device.userInterfaceIdiom; // 播放一个输入的声音
      [device playInputClick]; // UIInputViewAudioFeedback 协议方法
      /*
      实现该方法,返回 YES 则自定义的视图能够播放输入的声音
      */
      - (BOOL)enableInputClicksWhenVisible { return YES;
      }
  • Swift

    • 设备基本信息

      	// 获取当前设备
      let device:UIDevice = UIDevice.currentDevice() // 获取设备名称
      /*
      e.g. "My iPhone"
      */
      let name:String = device.name // 获取设备类型
      /*
      e.g. @"iPhone", @"iPod touch"
      */
      let model:String = device.model // 获取本地化设备类型
      /*
      localized version of model
      */
      let localizedModel:String = device.localizedModel // 获取设备系统名称
      /*
      e.g. @"iOS"
      */
      let systemName:String = device.systemName // 获取设备系统版本
      /*
      e.g. @"4.0"
      */
      let systemVersion:String = device.systemVersion // 获取设备 UUID
      /*
      可用于唯一标识该设备,同一供应商不同应用具有相同的 UUID
      */
      let identifierForVendor:NSUUID = device.identifierForVendor!
      let UUID:String = identifierForVendor.UUIDString
    • 设备方向

      	// 判断设备是否生成设备转向通知
      let generatesDeviceOrientationNotifications:Bool = device.generatesDeviceOrientationNotifications // 开启设备转向通知 /*
      通过调用该方法通知设备:如果用户改变了设备的朝向,我们想获悉这一点
      在注册设备方向通知时,需要先调用该方法
      */
      device.beginGeneratingDeviceOrientationNotifications() // 停止设备转向通知
      /*
      在移除设备方向通知后,需要调用该方法
      */
      device.endGeneratingDeviceOrientationNotifications() // 注册屏幕方向变化通知
      NSNotificationCenter.defaultCenter().addObserver(self,
      selector: #selector(UiDevice.deviceOrientationDidChange),
      name: UIDeviceOrientationDidChangeNotification,
      object: nil) // 获取设备方向
      /*
      UIDeviceOrientationUnknown,
      UIDeviceOrientationPortrait, // home button on the bottom 竖向,头向上
      UIDeviceOrientationPortraitUpsideDown, // home button on the top 竖向,头向下
      UIDeviceOrientationLandscapeLeft, // home button on the right 横向,头向左
      UIDeviceOrientationLandscapeRight, // home button on the left 横向,头向右
      UIDeviceOrientationFaceUp, // face up 平放,屏幕朝上
      UIDeviceOrientationFaceDown // face down 平放,屏幕朝下 除非正在生成设备方向的通知,否则返回 UIDeviceOrientationUnknown
      */
      let orientation:UIDeviceOrientation = UIDevice.currentDevice().orientation
    • 设备电池监控

      	// 判断设备是否开启电池监控
      /*
      default is NO
      */
      let batteryMonitoringEnabled:Bool = device.batteryMonitoringEnabled // 开启电池监控
      /*
      default is NO
      */
      device.batteryMonitoringEnabled = true // 获取设备电池状态
      /*
      UIDeviceBatteryStateUnknown,
      UIDeviceBatteryStateUnplugged, // on battery, discharging 未充电
      UIDeviceBatteryStateCharging, // plugged in, less than 100% 正在充电
      UIDeviceBatteryStateFull, // plugged in, at 100% 满电 如果禁用电池监控,则电池状态为 UIDeviceBatteryStateUnknown
      */
      let batteryState:UIDeviceBatteryState = device.batteryState // 注册电池状态变化通知
      NSNotificationCenter.defaultCenter().addObserver(self,
      selector: #selector(UiDevice.deviceBatteryStateDidChange),
      name: UIDeviceBatteryStateDidChangeNotification,
      object: nil) // 获取电池电量
      /*
      0 .. 1.0. 如果电池状态为 UIDeviceBatteryStateUnknown,百分比为 -1.0
      */
      let batteryLevel:Float = device.batteryLevel // 注册电池电量变化通知
      NSNotificationCenter.defaultCenter().addObserver(self,
      selector: #selector(UiDevice.deviceBatteryLevelDidChange),
      name: UIDeviceBatteryLevelDidChangeNotification,
      object: nil)
    • 设备接近状态监控

      	// 判断设备是否开启接近状态监控
      /*
      default is NO
      */
      let proximityMonitoringEnabled:Bool = device.proximityMonitoringEnabled // 开启接近状态监控
      /*
      default is NO
      */
      device.proximityMonitoringEnabled = true // 获取接近状态
      /*
      如果设备不具备接近感应器,则总是返回 NO
      */
      let proximityState:Bool = device.proximityState // 注册接近状态变化通知
      NSNotificationCenter.defaultCenter().addObserver(self,
      selector: #selector(UiDevice.deviceProximityStateDidChange),
      name: UIDeviceProximityStateDidChangeNotification,
      object: nil)
    • 设备性能

      	// 判断设备是否支持多任务
      let multitaskingSupported:Bool = device.multitaskingSupported
    • 设备操作

      	// 获取用户界面模式
      /*
      UIUserInterfaceIdiomUnspecified = -1,
      UIUserInterfaceIdiomPhone // iPhone and iPod touch style UI iPhone 和 iPod touch 风格
      UIUserInterfaceIdiomPad // iPad style UI iPad 风格
      UIUserInterfaceIdiomTV // Apple TV style UI Apple TV 风格
      UIUserInterfaceIdiomCarPlay // CarPlay style UI CarPlay 风格
      */
      let userInterfaceIdiom:UIUserInterfaceIdiom = device.userInterfaceIdiom // 播放一个输入的声音
      device.playInputClick()

2、App 应用相关信息的获取

  • NSBundle

  • Objective-C

    	NSDictionary *dicInfo = [[NSBundle mainBundle] infoDictionary];
    CFShow((__bridge CFTypeRef)(dicInfo)); // App 应用名称
    NSString *appName = [dicInfo objectForKey:@"CFBundleName"]; // App 应用版本
    NSString *appVersion = [dicInfo objectForKey:@"CFBundleShortVersionString"]; // App 应用 Build 版本
    NSString *appBuild = [dicInfo objectForKey:@"CFBundleVersion"];
  • Swift

    	let dicInfo:[String : AnyObject]? = NSBundle.mainBundle().infoDictionary
    CFShow(dicInfo) // App 应用名称
    let appName = dicInfo!["CFBundleName"] // App 应用版本
    let appVersion = dicInfo!["CFBundleShortVersionString"] // App 应用 Build 版本
    let appBuild = dicInfo!["CFBundleVersion"]

3、用户的本地化信息的获取

  • NSLocale

  • Objective-C

    	// 语言 en
    NSString *language = [[NSLocale preferredLanguages] objectAtIndex:0]; // 国家 en_US
    NSString *country = [[NSLocale currentLocale] localeIdentifier];
  • Swift

    	// 语言 en
    let language:String = NSLocale.preferredLanguages()[0] // 国家 en_US
    let country:String = NSLocale.currentLocale().localeIdentifier

4、获取设备唯一标识

  • iOS系统中,获取设备唯一标识的方法有很多:

    • 1、UDID:UDID 的全称是 Unique Device Identifier Description,顾名思义,它就是苹果 iOS 设备的唯一识别码。

      • UDID 是由子母和数字组成的 40 个字符串的序号,用来区别每一个唯一的 iOS 设备,包括 iPhones, iPads, 以及 iPod touches,这些编码看起来是随机的,实际上是跟硬件设备特点相联系的。
      • UDID 可以关联其它各种数据到相关设备上。例如,连接到开发者账号,可以允许在发布前让设备安装或测试应用;也可以让开发者获得 iOS 测试版进行体验。苹果用 UDID 连接到苹果的 ID,这些设备可以自动下载和安装从 App Store 购买的应用、保存从 iTunes 购买的音乐、帮助苹果发送推送通知、即时消息。 在 iOS 应用早期,UDID 被第三方应用开发者和网络广告商用来收集用户数据,可以用来关联地址、记录应用使用习惯,以便推送精准广告。
    • 2、OPEN UDID

      • iOS 2.0 版本以后 UIDevice 提供一个获取设备唯一标识符的方法 uniqueIdentifier,通过该方法我们可以获取设备的序列号,这个也是目前为止唯一可以确认唯一的标示符。许多开发者把 UDID 跟用户的真实姓名、密码、住址、其它数据关联起来;网络窥探者会从多个应用收集这些数据,然后顺藤摸瓜得到这个人的许多隐私数据。同时大部分应用确实在频繁传输 UDID 和私人信息。 为了避免集体诉讼,苹果最终决定在 iOS 5 的时候,将这一惯例废除,开发者被引导生成一个唯一的标识符,只能检测应用程序,其他的信息不提供。现在应用试图获取 UDID 已被禁止且不允许上架。所以这个方法作废。
    • 3、UUID:UUID 是 Universally Unique Identifier 的缩写,中文意思是通用唯一识别码.

      • 我们可以获取到 UUID,然后把 UUID 保存到 KeyChain 里面。这样以后即使 App 删了再装回来,也可以从 KeyChain 中读取回来。使用 group 还可以可以保证同一个开发商的所有程序针对同一台设备能够获取到相同的不变的 UDID。但是刷机或重装系统后 UDID 还是会改变。
    • 4、MAC Address

      • 使用 WiFi 的 mac 地址来取代已经废弃了的 uniqueIdentifier 方法。
      • 然而在 iOS 7 中苹果再一次无情的封杀 mac 地址,使用之前的方法获取到的 mac 地址全部都变成了 02:00:00:00:00:00。
    • 5、IDFA:广告标示符(IDFA-identifierForIdentifier),iOS6.0 及以后

      • 全名 advertisingIdentifier。直译就是广告 id, 在同一个设备上的所有 App 都会取到相同的值,是苹果专门给各广告提供商用来追踪用户而设的,用户可以在 设置|隐私|广告追踪 里重置此id的值,或限制此 id 的使用,故此 id 有可能会取不到值,但好在 Apple 默认是允许追踪的,而且一般用户都不知道有这么个设置,所以基本上用来监测推广效果,是戳戳有余了。由于 IDFA 会出现取不到的情况,故绝不可以作为业务分析的主 id,来识别用户。

      • 获取代码:

        	#import <AdSupport/AdSupport.h>
        NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
      • 从 17 年 2 月初开始,Apple 开始拒绝采集 IDFA(identifier for advertising)而未集成任何广告服务的应用进入 AppStore。

    • 6、IDFV:应用提供商标示符(IDFV-identifierForVendor),iOS6.0 及以后

      • 全名 identifierForVendor。顾名思义,是给 Vendor 标识用户用的,每个设备在所属同一个 Vender 的应用里,都有相同的值。其中的 Vender 是指应用提供商,但准确点说,是通过 BundleID 的反转的前两部分进行匹配,如果相同就是同一个 Vender,例如对于 com.taobao.app1, com.taobao.app2 这两个 BundleID 来说,就属于同一个 Vender,共享同一个 idfv 的值。和 IDFA 不同的是,IDFV 的值是一定能取到的,所以非常适合于作为内部用户行为分析的主 id,来标识用户,替代 OpenUDID。如果用户将属于此 Vender 的所有 App 卸载,则 IDFV 的值会被重置,即再重装此 Vender 的 App,IDFV 的值和之前不同。

      • 获取代码:

        	NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
  • 上面所有这些表示设备唯一的标识,在 iOS 7 中要么被禁止使用,要么重新安装程序后两次获取的标识符不一样。由于 iOS 系统存储的数据都是在 sandBox 里面,一旦删除 App,sandBox 也不复存在。

  • iOS 中获取设备唯一标示符的方法一直随版本的更新而变化。iOS 2.0 版本以后 UIDevice 提供一个获取设备唯一标识符的方法 uniqueIdentifier,通过该方法我们可以获取设备的序列号,这个也是目前为止唯一可以确认唯一的标示符。好景不长,因为该唯一标识符与手机一一对应,苹果觉得可能会泄露用户隐私,所以在 iOS 5.0 之后该方法就被废弃掉了;iOS 6.0 系统新增了两个用于替换 uniqueIdentifier 的接口,分别是:identifierForVendor,advertisingIdentifier,但这两个接口会在应用重新安装时改变数值,并不是唯一的标示符,所以开发者改为使用 WiFi 的 mac 地址来取代;iOS 7 中苹果又封杀 mac 地址,所以开发者再次改变思路使用 KeyChain 来保存获取到的 UDID,这样以后即使 App 删了再装回来,也可以从 KeyChain 中读取回来。

  • 首先保存设备的 UUID,可以使用类方法 + (id)UUID 是一个类方法,调用该方法可以获得一个 UUID。通过下面的代码可以获得一个 UUID 字符串:

     	NSString *uuid = [[NSUUID UUID] UUIDString];
  • 也可以保存在 iOS 6 中新增的 Vindor 标示符 (IDFV-identifierForVendor),获取这个 IDFV 的新方法被添加在已有的 UIDevice 类中。跟 advertisingIdentifier 一样,该方法返回的是一个 NSUUID 对象。

    	NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
  • 如果用户卸载了同一个 vendor 对应的所有程序,然后在重新安装同一个 vendor 提供的程序,此时 identifierForVendor 会被重置,所以这里要用到 KeyChain 来保存。KeyChain(钥匙串)是使用苹果设备经常使用的,通常要调试的话,都得安装证书之类的,这些证书就是保存在 KeyChain 中,还有我们平时浏览网页记录的账号密码也都是记录在 KeyChain 中。iOS 中的 KeyChain 相比 OS X 比较简单,整个系统只有一个 KeyChain,每个程序都可以往 KeyChain 中记录数据,而且只能读取到自己程序记录在 KeyChain 中的数据。iOS 中 Security.framework 框架提供了四个主要的方法来操作 KeyChain:

    	// 查询 OSStatus
    SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result); // 添加 OSStatus
    SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result); // 更新 KeyChain 中的 ItemOSStatus
    SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate); // 删除 KeyChain 中的 ItemOSStatus
    SecItemDelete(CFDictionaryRef query)
  • 这四个方法参数比较复杂,一旦传错就会导致操作 KeyChain 失败,文档中介绍的比较详细,大家可以查查官方文档。而苹果提供的 KeyChain 使用起来略麻烦,所以这里推荐一个第三方库 SSKeychain/SAMKeychain 对苹果安全框架 API 进行了简单封装,支持对存储在钥匙串中密码、账户进行访问,包括读取、删除和设置。SSKeyChains 使用简单,通过实例代码便可掌握。

    	// 保存一个 UUID 字符串到钥匙串:
    CFUUIDRef uuid = CFUUIDCreate(NULL);
    assert(uuid != NULL);
    CFStringRef uuidStr = CFUUIDCreateString(NULL, uuid); [SSKeychain setPassword:[NSString stringWithFormat:@"%@", uuidStr] forService:@"com.yourapp.yourcompany"account:@"user"]; // 从钥匙串读取 UUID:
    NSString *retrieveuuid = [SSKeychainpasswordForService:@"com.yourapp.yourcompany"account:@"user"]; // 注意: setPassword 和 passwordForSevice 方法中的 services 和 accounts 参数应该是一致的。
  • 基本的实现思路便是这样,下面是具体的一种具体实现代码,仅供参考。

    	+ (NSString *)getDeviceId {
    
    		NSString *currentDeviceUUIDStr = [SSKeychain passwordForService:[NSBundle mainBundle].bundleIdentifier account:@"uuid"];
    
    		if (currentDeviceUUIDStr == nil || [currentDeviceUUIDStr isEqualToString:@""]) {
    
    			NSUUID *currentDeviceUUID  = [UIDevice currentDevice].identifierForVendor;
    currentDeviceUUIDStr = currentDeviceUUID.UUIDString;
    currentDeviceUUIDStr = [currentDeviceUUIDStr stringByReplacingOccurrencesOfString:@"-" withString:@""];
    currentDeviceUUIDStr = [currentDeviceUUIDStr lowercaseString];
    [SSKeychain setPassword: currentDeviceUUIDStr forService:[NSBundle mainBundle].bundleIdentifier account:@"uuid"];
    }
    return currentDeviceUUIDStr;
    }
04-18 19:26