# 1:背景

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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 是如何进行计算的,比如这行代码

1
2
3
4
int a = 35;
int b = 47;
int c = a + b;


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

1
2
3
4
5
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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,运算结果都是一样的。

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

1
2
3
4
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 <br> ------- => 1011<br>1011
按位与&0011 <br> ------- => 1011<br>1011
按位取反~0011 => 1100
按位异或 (相同为零不同为一)^0011 <br> ------- => 1000<br>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

# 转换代码

先列出代码,后续解析

1
2
3
4
5
6
7
8
9
- (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,所以这一步我们既然要取出红色值就要把其他位全部置零来排除干扰,这步操作便是如此,在计算机系统内是二进制来实现的,即:<br>
    1111 1111 1010 0001 0011 0001<br>------------------------------------------- => & => 1111 1111 0000 0000 0000<br>
    1111 1111 0000 0000 0000 0000<br> 这部操作做完后可以看出将除了 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:枚举

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 中我们可以利用异或来进行加解密,异或的特性如下

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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 两个个变量,做如下位运算操作

1
2
3
4
5
6
void exchange(int x , int y)
{
x ^= y;
y ^= x;
x ^= y;
}

  • 判断一个数的奇偶性,其实我们可以用 **%2** 来判断,代码量不高,但是之前讲过,除法运算的时钟周期非常多,所以代码虽然不多并不代表效率高,我们可以用如下运算来完成:

1
2
3
4
5
6
7
8
void test(int x)
{
if (x&1) {
printf("奇数");
} else {
printf("偶数");
}
}

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

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

1
2
3
4
int average(int x, int y)
{
return (x&y)+((x^y)>>1);
}

# 1000:总结

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

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Molier 微信支付

微信支付

Molier 支付宝

支付宝