iOS程序猿必知的位运算相关知识

从现代计算机电路来讲,只有 通电/没电 两种状态,即为 0/1 状态,计算机中全部的数据按照具体的编码格式以二进制的形式存储在设备中。ios

  直接操做这些二进制数据的位数据就是位运算,在iOS开发中基本全部的位运算都经过枚举声明传值的方式将位运算的实现细节隐藏了起来:算法

typedef NS_OPTIONS(NSUInteger, UIRectEdge) {数组

UIRectEdgeNone = 0,服务器

UIRectEdgeTop = 1 << 0,ide

UIRectEdgeLeft = 1 << 1,工具

UIRectEdgeBottom = 1 << 2,优化

UIRectEdgeRight = 1 << 3,this

UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight编码

} NS_ENUM_AVAILABLE_IOS(7_0);atom

  位运算是一种极为高效乃至能够说最为高效的计算方式,虽然现代程序开发中编译器已经为咱们作了大量的优化,可是合理的使用位运算能够提升代码的可读性以及执行效率。

基础计算

  在了解怎么使用位运算以前,笔者简单说一下CPU处理计算的过程。若是你对 CPU的计算方式有所了解,能够跳过这一节。

  当代码 int sum = 11 + 79 被执行的时候,计算机直接将两个数的二进制位进行相加和进位操做:

11: 0 0 0 0 1 0 1 179: 0 1 0 0 1 1 1 1

————————————————————90: 0 1 0 1 1 0 1 0

  一般来讲CPU执行两个数相加操做所花费的时间被咱们称做一个时钟周期,而2.0GHz频率的CPU表示能够在一秒执行运算2.0*1024*1024*1024 个时钟周期。相较于加法运算,下面看一下 11*2 、 11*4 的二进制结果:

11: 0 0 0 0 1 0 1 1 * 2

————————————————————22: 0 0 0 1 0 1 1 0

11: 0 0 0 0 1 0 1 1 * 4

————————————————————44: 0 0 1 0 1 1 0 0

  简单来讲,不难发现当某个数乘以 2的N次幂 的时候,结果等同于将这个数的二进制位置向左移动 N 位,在代码中咱们使用 num << N 表示将 num 的二进制数据左移N 个位置,其效果等同于下面这段代码:

for (int idx = 0; idx < N; idx++) {

num *= 2;

}

  假如相乘的两个数都不是 2的N次幂 ,这时候编译器会将其中某个值分解成多个 2的N次幂 相加的结果进行运算。好比 37 * 69 ,这时候CPU会将 37 分解成 32+4+1,而后换算成 (69<<5) + (69<<2) + (69<<0)的方式计算出结果。所以,计算两个数相乘一般须要十个左右的时钟周期。 同理,代码 num >> N 的做用等效于:

for (int idx = 0; idx < N; idx++) {

num /= 2;

}

  可是两个数相除花费的时钟周期要比乘法还要多得多,其大部分消耗在将数值分解成多个 2的N次幂 上。除此以外,浮点数涉及到的计算更为复杂,这里也简单聊聊浮点数的准确度问题。拿 float 类型来讲,总共使用了 32bit 的存储空间,其中第一位表示正负, 2~13位 表示整数部分的值, 14~32位 之中分别存储了小数位以及科学计数的标识值(这里可能并不那么准确,主要是为了给读者一个大概的介绍)。因为小数位的二进制数据依旧保持 2的N次幂 特性,假以下面的二进制属于小数位:

  那么这部分小数位的值等于: 1/2 + 1/4 + 1/8 + 1/16 + 1/128 = 0.9453125。所以,当你把一个没有任何规律的小数例如3.1415926535898 存入计算机的时候,小数点后面会被拆解成不少的 2的N次幂 进行保存。因为小数位老是有限的,所以当分解的 N 超出这些位数时致使存储不下,就会出现精度误差。另外一方面,这样的分解计算势必要消耗大量的时钟周期,这也是大量的浮点数运算 (cell动态计算) 容易引起卡顿的缘由。因此,当小数位过多时,改用字符串存储是一个更优的选择。

位运算符

  使用的运算符包括下面:

含义运算符

 

& 操做

0 0 1 0 1 1 1 0 46

1 0 0 1 1 1 0 1 157

———————————————

0 0 0 0 1 1 0 0 12

 

   操做

0 0 1 0 1 1 1 0 46

1 0 0 1 1 1 0 1 157

———————————————

1 0 1 1 1 1 1 1 191

 

~ 操做

0 0 1 0 1 1 1 0 46

———————————————

1 1 0 1 0 0 0 1 225

 

^ 操做

0 0 1 0 1 1 1 0 46

1 0 0 1 1 1 0 1 157

———————————————

1 0 1 1 0 0 1 1 179

 

色彩存储

  使用位运算包括下面几个缘由: 一、代码更简洁 二、更高的效率 三、更少的内存

  简单来讲,咱们如何单纯的保存一张 RGB 色彩空间下的图片?因为图片由一系列的像素组成,每一个像素有着本身表达的颜色,所以须要这么一个类用来表示图片的单个像素:

@interface Pixel

@property (nonatomic, assign) CGFloat red;@property (nonatomic, assign) CGFloat green;@property (nonatomic, assign) CGFloat blue;@property (nonatomic, assign) CGFloat alpha;

@end

  那么在4.7寸的屏幕上,启动图须要 750*1334 个这样的类,不计算其余数据,单单是变量的存储须要 750*1334*4*8 = 32016000 个字节的占用内存。但实际上咱们使用到的图片老是将 RGBA 这四个属性保存在一个 int 类型或者其它类似的少字节变量中。

  因为色彩取值范围为 0~255 ,即 2^1 ~ 2^8-1 不超过一个字节的整数占用内存。所以能够经过左移运算保证每个字节只存储了一个决定色彩的值:

- (int)rgbNumberWithRed: (int)red green: (int)green blue: (int)blue alpha: (float)alpha {

int bitPerByte = 8;

int maxNumber = 255;

int alphaInt = alpha * maxNumber;

int rgbNumber = (red << (bitPerByte*3)) + (green << (bitPerByte*2)) + (blue << bitPerByte) + alphaInt;

}

  同理,经过右移操做保证数值的最后一个字节存储着须要的数据,并用 0xff 将值取出来:

- (void)obtainRGBA: (int)rgbNumber {

int mask = 0xff;

int bitPerByte = 8;

double alphaInt = (rgbNumber & mask) / 255.0;

int blue = ((rgbNumber >> bitPerByte) & mask);

int green = ((rgbNumber >> (bitPerByte*2)) & mask);

int red = ((rgbNumber >> (bitPerByte*3)) & mask);

}

  对比使用类和位运算存储,效率跟内存占用上能够说是完败。

位运算应用

  苹果在类对象的结构中使用了位运算这一设计:每一个对象都有一个整型类型的标识符 flags ,其中多个不一样的位表示了是否存在弱引用、是否被初始化等信息,对于这些存储的数据经过 & 、 | 等运算符获取出来。这些在 runtime源码 中都能看到,下面是一段伪代码(参数请勿对号入座)

#define IS_TAGGED_POINTER (1 << 12);

#define HAS_WEAK_REFERENCE (1 << 13);

inline void objc_object::free() {

if (this->flags | HAS_WEAK_REFERENCE) {

/// set all weak reference point to nil

}

}

inline int objc_object::retainCount() {

if (this.flags | IS_TAGGED_POINTER) {

return (int)INT_MAX;

else {

return this->retainCount;

}

}

......

  借鉴苹果的运算操做,能够声明一个应用经常使用权限的枚举,来获取咱们的应用权限:

typedef NS_ENUM(NSInteger, LXDAuthorizationType)

{

LXDAuthorizationTypeNone = 0,

LXDAuthorizationTypePush = 1 << 0, ///< 推送受权

LXDAuthorizationTypeLocation = 1 << 1, ///< 定位受权

LXDAuthorizationTypeCamera = 1 << 2, ///< 相机受权

LXDAuthorizationTypePhoto = 1 << 3, ///< 相册受权

LXDAuthorizationTypeAudio = 1 << 4, ///< 麦克风受权

LXDAuthorizationTypeContacts = 1 << 5, ///< 通信录受权

};

  经过声明一个全局的权限变量来保存不一样的受权信息。当应用拥有对应的受权时,经过 | 操做符保证对应的二进制位的值被修改为 1 。不然对对应受权枚举进行~ 取反后再 & 操做消除二进制位的受权表达。为了完成这些工做,创建一个工具类来获取以及更新受权的状态:

/*!

* @brief 获取应用受权信息工具,最低使用版本:iOS8.0

*/NS_CLASS_AVAILABLE_IOS(8_0) @interface LXDAuthObtainTool : NSObject

/// 获取当前应用权限

+ (LXDAuthorizationType)obtainAuthorization;/// 更新应用权限

+ (void)updateAuthorization;

@end

#pragma mark - LXDAuthObtainTool.mstatic LXDAuthorizationType kAuthorization;

@implementation LXDAuthObtainTool

+ (void)initialize

{

kAuthorization = LXDAuthorizationTypeNone;

[self updateAuthorization];

}

/// 获取当前应用权限

+ (LXDAuthorizationType)obtainAuthorization

{

return kAuthorization;

}

/// 更新应用权限

+ (void)updateAuthorization

{

/// 推送

if ([UIApplication sharedApplication].currentUserNotificationSettings.types == UIUserNotificationTypeNone) {

kAuthorization &= (~LXDAuthorizationTypePush);

else {

kAuthorization |= LXDAuthorizationTypePush;

}

/// 定位

if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse) {

kAuthorization |= LXDAuthorizationTypeLocation;

else {

kAuthorization &= (~LXDAuthorizationTypeLocation);

}

/// 相机

if ([AVCaptureDevice authorizationStatusForMediaType: AVMediaTypeVideo] == AVAuthorizationStatusAuthorized) {

kAuthorization |= LXDAuthorizationTypeCamera;

else {

kAuthorization &= (~LXDAuthorizationTypeCamera);

}

/// 相册

if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) {

kAuthorization |= LXDAuthorizationTypePhoto;

else {

kAuthorization &= (~LXDAuthorizationTypePhoto);

}

/// 麦克风

[[AVAudioSession sharedInstance] requestRecordPermission: ^(BOOL granted) {

if (granted) {

kAuthorization |= LXDAuthorizationTypeAudio;

else {

kAuthorization &= (~LXDAuthorizationTypeAudio);

}

}];

/// 通信录

if ([UIDevice currentDevice].systemVersion.doubleValue >= 9) {

if ([CNContactStore authorizationStatusForEntityType: CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) {

kAuthorization |= LXDAuthorizationTypeContacts;

else {

kAuthorization &= (~LXDAuthorizationTypeContacts);

}

else {

if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {

kAuthorization |= LXDAuthorizationTypeContacts;

else {

kAuthorization &= (~LXDAuthorizationTypeContacts);

}

}

}

@end

  在咱们须要使用某些受权的时候,例如打开相册时,直接使用 & 运算符判断权限便可:

- (void)openCamera {

LXDAuthorizationType type = [LXDAuthObtainTool obtainAuthorization];

if (type & LXDAuthorizationTypeCamera) {

/// open camera

else {

/// alert

}

}

  在数据存储的方面位运算拥有着占用内存少,高效率的优势,固然位运算能作的不只仅是这些,好比笔者项目有这样的一个需求:用户登陆成功以后在首页界面请求服务器下载全部金额相关的数据。这个需求最大的问题是:

AFN2.3+ 版本的请求库不支持同步请求,当须要多个请求任务一次性执行时,判断请求任务完成是很麻烦的一件事情。

  因为 NSInteger 拥有8个字节64位的二进制位,所以笔者将每个二进制位用来表示单个任务请求的完成状态。已知登录后须要同步数据的接口为 N(<64)个,所以能够声明一个所有请求任务完成后的状态变量:

NSInteger complete = 0;for (int idx = 0; idx < N; idx++) {

complete |= (1 << idx);

}

  而后使用一个标志变量 flags 用来记录当前任务请求的完成状况,每个数据同步的任务完成以后对应的二进制位就置为 1 :

__block NSInteger flags = 0;NSArray* urls = @[......];NSArray* params = @[......];

for (NSInteger idx = 0; idx < urls.count; idx++) {

NSString * url = urls[idx];

NSDictionary * param = params[idx];

[LXDDataSyncTool syncWithUrl: url params: param complete: ^{

flags |= (1 << idx);

if ( (flags ^ complete) == 0 ) {

[self completeDataSync];

}

}];

}

位运算与算法

  在广泛使用高级语言开发的大环境下,位运算的实现更多的被封装起来,所以大多数开发者在项目开发中不见得会使用这一机制。在上面基础计算 一节中笔者说过两个数相加只须要一个时钟周期(虽然 CPU 从寄存器读取存放数据也须要额外的时钟周期,但一般这部分的花销老是常量级,能够忽略不计)

  因为位运算的处理基本也在一个时钟周期完成,位运算这一操做备受算法封装者的喜好。好比交换两个变量的值通常状况下代码是:

int sum = a;a = b;b = sum;

  又或者:

a = a + b;b = a - b;a = a - b;

  若是经过位运算的方式则不须要任何加减操做或者临时变量:

a ^= b;b = a ^ b;a = a ^ b;

  上面的代码和第二种方式的实现思路相似,都是将 a 和 b 合并成单个变量,再分别消除变量中的 a 和 b 的值( ^ 运算会对相同二进制位的值置0,意味着 b^b 的结果等于0)

  进阶题:找出整型数组中惟一的单独数字,数组中的其余数字的个数为2个

  经过上面不用中间变量交换 a 和 b 的值能够得出下面的最简代码:

- (int)singleDog(int * nums) {

int singleDog = 0;

for (int idx = 0; idx < sizeof(nums)/sizeof(int); idx++) {

singleDog ^= nums[idx];

}

return singleDog;

}

 

文章来源:Bison的技术博客

相关文章
相关标签/搜索