从现代计算机电路来看,只有高电平/低电平两种状态,即为0/1状态,计算机中所有的数据按照具体的编码格式以二进制的形式存储在设备中。
直接操作这些二进制数据的位数据就是位运算。在iOS中国呢基本上所有的位运算都是通过枚举声明传值的方式将位运算的真实细节隐藏了起来。
typedef NS_OPTIONS(NSUInteger, UIRectEdge) { UIRectEdgeNone = 0, UIRectEdgeTop = 1 << 0, UIRectEdgeLeft = 1 << 1, UIRectEdgeBottom = 1 << 2, UIRectEdgeRight = 1 << 3, UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight } NS_ENUM_AVAILABLE_IOS(7_0);
位运算是一种极为高效乃至可以说最为高效的计算方式,虽然现在程序开发中编译器已经为我们做了大量的优化,但是合理的使用位运算可以提高代码的可读性以及执行效率。
二进制的运算
二进制的运算算术:
加法: 0 + 0 = 0,0 + 1 = 1,1 + 0 = 1, 1 + 1 = 10(向高位进位)
减法: 0 - 0 = 0,10 - 1 = 1(向高位借位),1 - 0 = 1,1 - 1 = 0
乘法: 0 * 0 = 0,0 * 1 = 0,1 * 0 = 0,1 * 1 = 1
除法: 0 / 0 = 0,0 / 1 = 0,1 / 0 = 0(无意义), 1 / 1 = 1
基础计算
在了解怎么使用位运算之前,我们简单说一下CPU处理计算的过程,当代吗int sum = 11 + 79被执行的时候,计算机直接将两个数的二进制位进行相加和进位操作:
11: 0 0 0 0 1 0 1 1 79: 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,CUP会将37分解成32 + 4 + 1 然后(69 << 5) + (69 << 2) + (69 << 0)得出结果。 同理,代码num >> N
的作用等效于:
for (int idx = 0; idx < N; idx++) { num /= 2; }
位运算符
左移 << 各二进位全部左移若干位 高位丢弃 低位补0
右移 >> 各二进位全部右移若干位 对无符号数 高位补0 有符号数 各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)
按位或 | 两个位都为0时 结果才为0
按位与 & 两个位都为1时 结果才为1
按位取反 ~ 0变1 1变0
按位异或 ^ 两个位相同为0 相异为1
按位与运算符 &
定义:参加运算的两个数据 按二进制位进行与运算
运算规则:
0&0 = 0 0&1 = 0 1&0 = 0 1&1 = 1
总结:两位同时为1 结果才为1 否则结果为0
例如: 3&5即 0000 0011 & 0000 0101 = 0000 0001 即3&5 = 1
与运算的用途:
1> 清零
如果想将一个单元清零 即使其全部二进制为0 只要与一个各位都为零的数值相与 结果为0
2> 取一个数的指定位
比如取数x = 1010 1110的低四位 只需要另找一个数y 另y的低四位为1 其余位为0 即y = 0000 1111 然后将x与y进行按位与运算 即可得到x的指定位。
3> 判断奇偶
只要根据最末位是0还是1来决定 为0就是偶数 为1就是奇数。因此可以用if ((a & 1) == 0)
代替if (a % 2 == 0)
来判断a是不是偶数。
按位或运算符 |
定义:参加运算的两个对象 按二进制位进行或运算。
运算规则:
0 | 0 = 0, 0 | 1 = 1,1 | 0 = 0,1 | 1 = 1
总结:参加运算的两个对象只要有一个值为1 其值为1
例如 3|5 0000 0011 | 0000 0101 = 0000 0111 因此 3 | 5 = 7
或运算的用途:
1)常用来对一个数据的某些位设置为1
比如将数 X=1010 1110 的低4位设置为1,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行按位或运算(X|Y=1010 1111)即可得到。
异或运算符 ^
定义: 参加运算的两个数据 按二进制位进行异或运算
运算规则:
0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0
总结 参加运算的两个对象 如果两个相应位相同为0 相异为1
与0相异或 得到本身 与1相异或得到相反的数字
异或运算的用途:
1>按位异或运算 可以用来使某些特定的位翻转 如对数10100001的第1位和第2位翻转,可以将数与00000110进行按位异或运算。
例如: 10100001^00000110=10100111
2>实现两个值的交换
例如:
a = 1010 0001 b = 0000 0110
a = a ^ b; //a = 1010 0111
b = b ^ a; //b = 1010 0001
a = a ^ b; //a = 0000 0110
取反运算符 ~
定义: 参加运算的一个数据 按二进制进行取反运算
运算规则:
~1 = 0; ~0 = 1;
总结:对一个二进制数按位取反 即将0变1 1变0
异或运算的用途:
1>使一个数的最低位为零
使a的最低位为0 可以表示为 a & ~1
左移运算符 <<
定义: 讲一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃 右边补0)
a = 1010 1110, a << 2 将a的二进制位左移二位 右补0 既得 a = 1011 1000
若左移时舍弃的高位不包含1 则每左移一位 相当于该数乘以2
右移运算符 >>
定义:将一个运算对象的各二进制位全部右移若干位 正数左补零 负数左补1 右边丢弃
例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。
操作数每右移一位,相当于该数除以2。
位运算在iOS中的运用
色彩存储
我们如何保存一张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); }
位移枚举
iOS中的位移枚举可以实现多选
先创建一个普通的枚举
typedef NS_ENUM(NSUInteger, YLEnum) { YLEnumTop = 1, // 0000 0001 YLEnumBottom , // 0000 0010 YLEnumLeft , // 0000 0011 YLEnumRight , // 0000 0100 };
根据以往的开发经验可以知道 YLEnumTop
的枚举值是1
,YLEnumBottom
是2
, YLEnumLeft
是3
,YLEnumRight
是4
,后面的注释 是转化为二进制后的值。
再创建一个位移枚举 表示着 上,下,左,右 四个方向。
typedef NS_OPTIONS(NSUInteger, YLOptions) { YLOptionsTop = 1 << 0, // 0000 0001 YLOptionsBottom = 1 << 1, // 0000 0010 YLOptionsLeft = 1 << 2, // 0000 0100 YLOptionsRight = 1 << 3, // 0000 1000 };
经过位移移动后YLOptions的枚举值就可以看成是
typedef NS_OPTIONS(NSUInteger, YLOptions) { YLOptionsTop = 1, // 0000 0001 YLOptionsBottom = 2, // 0000 0010 YLOptionsLeft = 4, // 0000 0100 YLOptionsRight = 8, // 0000 1000 };
普通枚举NS_ENUM判断可以使用 if else 和 switch
位移枚举NS_OPTIONS的判断方法如下:
- (void)optionsDemo:(YLOptions)type{ if (type & YLOptionsTop) { NSLog(@"上 %ld",type & YLOptionsTop); } if (type & YLOptionsBottom) { NSLog(@"下 %ld",type & YLOptionsBottom); } if (type & YLOptionsLeft) { NSLog(@"左 %ld",type & YLOptionsLeft); } if (type & YLOptionsRight) { NSLog(@"右 %ld",type & YLOptionsRight); } }
方法的调用:
[self optionsDemo:YLOptionsTop | YLOptionsRight];
会有两个打印输出
Demo[20332:1760218] 上 1 Demo[20332:1760218] 右 8
这样就实现了多选功能。多个枚举值 要使用 | 按位或
解析方法的调用
方法调用:
[self optionsDemo:YLOptionsTop | YLOptionsRight];
type = YLOptionsTop | YLOptionsRight
多选时,用加法来进行枚举的叠加,减法来进行枚举的删除
//多选时,用加法来进行枚举的叠加,减法来进行枚举的删除 //根据打印可以知道 type == 9 // type 是 top 和 right 相加得来的 // 0000 0001 + 0000 1000 = 0000 1001 即为 9 - (void)optionsDemo:(YLOptions)type{ //根据下面的 按位运算 就可以计算出结果了 //1001 & 0001 1001 0001 0001 if (type & YLOptionsTop) { NSLog(@"上 %ld",type & YLOptionsTop); } //1001 & 0010 0000 if (type & YLOptionsBottom) { NSLog(@"下 %ld",type & YLOptionsBottom); } //1001 & 0100 0000 if (type & YLOptionsLeft) { NSLog(@"左 %ld",type & YLOptionsLeft); } //1001 & 1000 1000 if (type & YLOptionsRight) { NSLog(@"右 %ld",type & YLOptionsRight); } }