关于位运算看这个就够了

19,218 阅读10分钟

1:背景

从现代计算机中所有的数据二进制的形式存储在设备中。即0、1两种状态,计算机对二进制数据进行的运算(+、-、*、/)都是叫位运算,即将符号位共同参与运算的运算。

我们每一种语言最终都会通过编译器转换成机器语言来执行,所以直接使用底层的语言就不需要便编译器的转换工作从而得到更高的执行效率,当然可读性可能会降低,这也是为什么汇编在大部分情况下有更快的速度。项目中合理的运用位运算能提高我们代码的执行效率。

在iOS系统中位运算多见于枚举中,其他地方很少见,因为位运算是底层的计算机语言,而在iOS开发中不管是Objective—C还是Swift都属于高级的编程语言,大量的位运算都被苹果封装了起来,我们只关心调用的接口不用关心内部的实现。

typedef NS_OPTIONS(NSUInteger, NSLayoutFormatOptions) {
    NSLayoutFormatAlignAllLeft = (1 << NSLayoutAttributeLeft),
    NSLayoutFormatAlignAllRight = (1 << NSLayoutAttributeRight),
    NSLayoutFormatAlignAllTop = (1 << NSLayoutAttributeTop),
    NSLayoutFormatAlignAllBottom = (1 << NSLayoutAttributeBottom),
    NSLayoutFormatAlignAllLeading = (1 << NSLayoutAttributeLeading),
    NSLayoutFormatAlignAllTrailing = (1 << NSLayoutAttributeTrailing),
.
.
.
.
    }

10:计算机计算原理

####加法和乘法

举一个简单的例子来看下CPU是如何进行计算的,比如这行代码

int a = 35;
int b = 47;
int c = a + b;

计算两个数的和,因为在计算机中都是以二进制来进行运算,所以上面我们所给的int变量会在机器内部先转换为二进制在进行相加

35:  0 0 1 0 0 0 1 1
47:  0 0 1 0 1 1 1 1
————————————————————
82:  0 1 0 1 0 0 1 0

再来看下乘法,执行如下的代码

int a = 3;
int b = 2;
int c = a * b;

3:  0 0 0 0 0 0 1 1  *  2
————————————————————
6:  0 0 0 0 0 1 1 0

*********************************************

int a = 3;
int b = 4;
int c = a * b;

3:  0 0 0 0 0 0 1 1  *  4
————————————————————
12:  0 0 0 0 1 1 0 0

*********************************************

int a = 3;
int b = 8;
int c = a * b;

3:  0 0 0 0 0 0 1 1  *  8
————————————————————
24:  0 0 0 1 1 0 0 0

通过以上运算可以看出当用a乘b,且如果b满足2^N的时候 就相当于把a的二进制数据向左移动N位,放到代码中 我们可以这样来写 a << N,所以上面3 * 2、3 * 4、3 * 8其实是可以写成3<<1、3<<2、3<<3,运算结果都是一样的。

那假如相乘的两个数都不满足2^N怎么办呢?其实这个时候编译器会将其中一个数拆分成多个满足2^N的数相加的情况,打个比方

int a = 15;				int a = 15
int b = 13;      =>    	int b = (4 + 8 + 1)
int c = a * b;			int c = a * b	

最后其实执行相乘运算就会变成这样 15 * 4 + 15 * 8 + 15 * 1,按照上文说的移位来转换为位运算就会变成15 << 2 + 15 << 3 + 15 << 0

####减法和除法 减法也是与加法同理只不过计算机内减法操作就是加上一个数的负数形式,且在操作系统中都是以补码的形式进行操作(因为正数的源码补码反码都与本身相同)。首先, 因为人脑可以知道第一位是符号位, 在计算的时候我们会根据符号位, 选择对真值区域的加减. 但是对于计算机, 加减乘数已经是最基础的运算, 要设计的尽量简单. 计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参与运算的方法. 我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了.

除法的话其实和乘法原理相同,不过乘法是左移而除法是右移,但是除法的计算量要比乘法大得多,其大部分的消耗都在拆分数值,和处理小数的步骤上,所以如果我们在进行生成变量的时候如果遇到多位的小数我们尽量把他换成string的形式,这也是为什么浮点运算会消耗大量的时钟周期(操作系统中每进行一个移位或者加法运算的过程所消耗的时间就是一个时钟周期,3.0GHz频率的CPU可以在一秒执行运算3.010241024*1024个时钟周期)

11:位运算符

使用的运算符包括下面:

含义 运算符 例子
左移 << 0011 => 0110
右移 >> 0110 => 0011
按位或 0011
------- => 1011
1011
按位与 & 0011
------- => 1011
1011
按位取反 ~ 0011 => 1100
按位异或 (相同为零不同为一) ^ 0011
------- => 1000
1011

100:颜色转换

背景

上面说了iOS中经常见到的位运算的地方是在枚举中,那么颜色转换应该是除了枚举之外第二比较常用位运算的场景。打个比方设计师再给我们出设计稿的时候通常会在设计稿上按照16进制的样子给我们标色值。但是iOS中的UIColor并不支持使用十六进制的数据来初始化。所以我们需要将十六进制的色值转换为UIColor。

原理分析

UIColor中通常是用传入RGB的数值来初始化,而且每个颜色的取值范围是十进制下的0~255,而设计同学又给的是十六进制数据,所以在操作系统中需要把这两种进制的数据统一成二进制来进行计算,这就用到了位运算。这里用一个十六进制的色值来举例子比如0xffa131我们要转换就要先理解其组成

  • 0x或者0X:十六进制的标识符,表示这个后面是个十六进制的数值,对数值本身没有任何意义

  • ff 颜色中的R值,转换为二进制为 1111 1111

  • a1 颜色中的G值,转换为二进制为 1010 0001

  • 31 颜色中的B值,转换为二进制为 0011 0001

  • 上述色彩值转换为二进制后为1111 1111 1010 0001 0011 0001(每一位十六进制的对应4位二进制,如果位数不够记得高位补零)

通常来讲十六进制的颜色是按照上面的RGB的顺序排列的,但是并不固定,有时候可能会在其中加A(Alpha)值,具体情况按照设计为准,本文以通用情况举例。

综上,我们只需把对应位的值转换为10进制然后/255.0f就可得到RGB色彩值,从而转换为UIColor

####转换代码 先列出代码,后续解析

- (UIColor *)colorWithHex:(long)hexColor alpha:(float)opacity
{
	//将传入的十六进制颜色0xffa131 转换为UIColor
	
    float red = ((hexColor & 0xFF0000) >> 16)/255.0f;
    float green = ((hexColor & 0xFF00) >> 8)/255.0f;
    float blue = (hexColor & 0xFF)/255.0f;
    return [UIColor colorWithRed:red green:green blue:blue alpha:opacity];
}

大概原理可以看出将RGB每个值都解析出来然后变成UIColor,先拿第一步转换红色值来说,我们按照运算顺序一步步来讲(默认将参数代入,用0xffa131代替hexColor)

  • 0xffa131 & 0xFF0000

    我们知道红色值是前两位也就是ff,所以这一步我们既然要取出红色值就要把其他位全部置零来排除干扰,这步操作便是如此,在计算机系统内是二进制来实现的,即:
    1111 1111 1010 0001 0011 0001
    ------------------------------------------- => & => 1111 1111 0000 0000 0000
    1111 1111 0000 0000 0000 0000
    这部操作做完后可以看出将除了R值之外的G值B值全部置零了,但是离最终结果还差点,因为0xFF是1111 1111,而我们的结果后面多出了16个0,所以便有了第二步操作

  • >> 16

    将上一步得到的结果右移16位即得到0000 0000 0000 0000 1111 1111高位的零可以忽略,这也是最终的结果

  • / 255.0f

    这一步应该都知道UIColor中传入的数值范围在0~1,所以我们要做下转换

  • 后续的G值和B值都是一样的,只是大家注意位数就可以了,值得注意的是两个二进制数进行位运算一定保证两个数的位数相同,位数不够的那个数高位要用0补齐

101:枚举

关于枚举中使用位运算我们之前也讲过,下面我们自己来写一个枚举(伪代码)

typedef NS_OPTIONS(NSUInteger, TestOptions) {
     TestOptionOne     =    1 << 0, (000001)
    
  	 TestOptionTwo     =    1 << 1,	(000010)
    
	 TestOptionThree   =    1 << 2,	(000100)
    
	 TestOptionFour    =    1 << 3,	(001000)

	 TestOptionFive    =    1 << 4,	(010000)

	 TestOptionSix     =    1 << 5,	(100000)
.
.
.
.

  • 解析 上面的枚举我后面用括号表明了位移后对应的二进制的值。这样写枚举的好处是我可以对其中选项多选比如TestOptionOne | TestOptionTwo (000001 | 000010 => 000011) 或者有其他的自定义组合。

110:加密

在iOS中我们可以利用异或来进行加解密,异或的特性如下

A ^ B = C => C ^ A = B => C ^ B = A 

上文我们可以把A认为是需要加密的数据,B认为是密钥 C是加密后的数据 比如:

#include <stdio.h>
main()
{
   char a[]="MyPassword";        /*要加密的密码*/
   char b[]="cryptographic";     /*密钥*/
   int i;
   /*加密代码*/
   for(i=0;a[i]!='\0';i++)
a[i]=a[i]^b[i];
   printf("You Password encrypted: %s\n",a);
   /*解密代码*/
   for(i=0;a[i]!='\0';i++)
a[i]=a[i]^b[i];
   printf("You Password: %s\n",a);
  
}

111:其他应用

  • 记得iOS总有一道面试题在不使用第三个变量的情况下交换两个变量的值,这里用到异或的上面加解密中的特性。我有x、y两个个变量,做如下位运算操作
void exchange(int x , int y) 
{ 
    x ^= y; 
    y ^= x; 
    x ^= y; 
} 
  • 判断一个数的奇偶性,其实我们可以用**%2**来判断,代码量不高,但是之前讲过,除法运算的时钟周期非常多,所以代码虽然不多并不代表效率高,我们可以用如下运算来完成:
void test(int x)
{
    if (x&1) {
        printf("奇数");
    } else {
        printf("偶数");
    }
}

原理很简单,因为二进制是满二进一,一旦超过1就会变0并进一位,这时候和00001做**&**操作一定会为0,反之不为零。这样写效率会更高。

  • 计算两个数的平均值,通常我们都是(x+y)/2,先不考虑效率问题,这样还会引起一个其他的问题,那就是x+y的值很有可能溢出大于INT_MAX,所以我们采用位运算的办法来解决即可:
int average(int x, int y) 
{    
    return (x&y)+((x^y)>>1); 
} 

1000:总结

其实位运算的应用远远不止这些,在算法方面适当的使用还是很有帮助的。参考文章文章