平时位运算、二进制操作什么的用到的很少,最近工作中要为 Flutter 的 APP 对接一个 BLE 设备,其中涉及到了相关的内容,简单记录下对一段代码的理解 twemoji-270f

代码片段

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
29
30
31
32
33
34
35
public static int getBytesIndex(int bitPos) {
return bitPos / 8;
}

public static int getOffset(int bitPos) {
return bitPos % 8;
}

private int[] parserHeartData(byte[] data) {
int pIndex = 0;
byte[] samp = new byte[12];

for(int i = 4; i <= 15; ++i) {
byte tem = data[getBytesIndex(i)];
samp[pIndex++] = tem >> 7 - getOffset(i) & 1;
}

int[] points = new int[12];
pIndex = 0;

for(int i = 16; i <= 159; i += 8) {
byte tem = data[getBytesIndex(i)];
i += 8;
byte high4 = data[getBytesIndex(i)] >> 4 & 15;
byte low4 = data[getBytesIndex(i)] & 15;
points[pIndex] = samp[pIndex] << 12 | high4 << 8 | tem;
++pIndex;
i += 8;
tem = data[getBytesIndex(i)];
points[pIndex] = samp[pIndex] << 12 | low4 << 8 | tem;
++pIndex;
}

return points;
}

本段代码的意图:

  1. 传入的 data 的长度为20,记为 d0 ~ d19;
  2. 最终将产生12个 point 数据,每个 point 共 13bit 有效数据,记为 p0 ~ p11;
  3. d0 与 d1 视为第一组,其中:
    • d0 的第 4~7 位为标识和序号;
    • 第 0~3 位分别为 p3 ~p0 的最高位;
    • d1 的第 0~7 位分别为 p7 ~p11 的最高位;
  4. 后续 data 每三个为一组循环,以 d2 ~ d4 为例,其中:
    • d2 为 p0 的低8位
    • d4 为 p1 的低8位
    • d3 的高4位(第 4~7 位)为 p0 的第 8~11位
    • d3 的低4位(第 0~3 位)为 p1 的第 8~11位

所以去掉 d0 和 d1,剩下数据3个一组共有6组,每组产生两个 point,最终是12个 point,每个 point 的长度为 8 + 4 + 1 = 13。

逐行分析

首先是两个定位函数

由于每个 data 的长度为 8bit,所以20个 data 共计 160bit,那么对于传入的 bitPos:

1
2
3
4
5
6
7
8
9
10
// 整除长度 8 得到该bit位于data中的第几个byte
public static int getBytesIndex(int bitPos) {
return bitPos / 8;
}

// 用长度 8 取余得到该bit在所处d中的偏移量
// 这个偏移量是以高位为起点的
public static int getOffset(int bitPos) {
return bitPos % 8;
}

对应如下:

bitPos: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
bitVal: 1 0 0 1 0 1 1 0 1 0 0 1 0 1 1 0
bit: 7bit 6bit 5bit 4bit 3bit 2bit 1bit 0bit 7bit 6bit 5bit 4bit 3bit 2bit 1bit 0bit
offset: 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
byte: <= = = d 0 = = => <= = = d 1 = = =>

然后分析主解析逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private int[] parserHeartData(byte[] data) {
// 从第一个point开始处理
int pIndex = 0;
// 该数组用于临时存储每个point的最高位,所以长度和point的长度一样为12
byte[] samp = new byte[12];
// 从上面的说明和图示可知,从d0的3bit开始到d1的0bit共12个bit分别是12个point的最高位
// 对应的bitPos就是4~15,所以在此范围循环
for(int i = 4; i <= 15; ++i) {
// 根据bitPos找到所在data的下标,从data中取出该byte数据
byte tem = data[getBytesIndex(i)];
// 根据bitPos找到其在对应byte中的偏移,用7-offset得到的是该bit右移几位后将位于byte的最低位
// 然后执行右移操作,再和1执行按位与操作,这样得到的就是想要当前bitPos对应的bit的值
samp[pIndex++] = tem >> 7 - getOffset(i) & 1;
}
……
}
  1. tem >> 7 - getOffset(i) & 1可以获得预期结果,是因为运算优先级 加减操作 高于 位移 高于 按位与;
  2. 由于(byte)1的二进制为 0000 0001,所以任何数据 &1后,逐位与比较,高位全部因为和0与而变成0,最低位和1与可以保持该bit,所以可以实现提取数组的最低位;
  3. 之后代码中的 & 15同理,因为(byte)15的二进制为 0000 1111,所以数据&15 后得到的是该数据的低4位。
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
// 接下来的这两行,先是定义了长度为12的数组,用于存储最终的 points,然后将脚标重置为零
int[] points = new int[12];
pIndex = 0;

// 由于前两个byte已经处理,所以略过前16个bit,从bitPos = 16处开始循环到最后
// 每次循环结束后bitPos+8,包括循环体里的两次+8,总共是三个byte
for(int i = 16; i <= 159; i += 8) {
// 取出该组的第一个byte
byte tem = data[getBytesIndex(i)];
// bitPos+8,所以下面操作的是该组的第二个byte
i += 8;
// 用和上面一样的操作,先右移四位再取低4bit,得到的就是该byte的高4位
byte high4 = data[getBytesIndex(i)] >> 4 & 15;
// 不位移直接 &15,得到该byte的低四位
byte low4 = data[getBytesIndex(i)] & 15;

// 根据规则,用之前保存的最高位、该组第二个byte的高四位和第1个byte组成该组生成的第1个point
points[pIndex] = samp[pIndex] << 12 | high4 << 8 | tem;

// 处理下一个point,bitPos 再一次 +8
++pIndex;
i += 8;
tem = data[getBytesIndex(i)];
// 用之前保存的最高位、该组第二个byte的低四位和第3个byte组成该组生成的第2个point
points[pIndex] = samp[pIndex] << 12 | low4 << 8 | tem;
++pIndex;
}

1.由于左移操作后,数据的低位都将置为0,例如 1010 1101 << 3 的结果为 0110 1000,按位或操作遇到 0 可以保留与这个 0 或的 bit 值,所以 samp[pIndex] << 12(tem 和 high4 的长度 ) | high4 << 8(tem 的长度) | tem 就完成了bit的组装,得到了最终的数据
2.根据 程序加法运算的二进制原理 很容易可以看出,如果不是用 | 操作。而是用 + 操作将三个部分合起来,实际指令的数量应该是一样的;但是这里还是应该用 | 操作,还是因为运算优先级 加减操作 高于 位移 高于 按位或,如果用 + 则需要把上面的代码改为 (samp[pIndex] << 12) + (low4 << 8) + tem

Android 中常见的用法

关于 | 操作符号,在 Android 开发中经常用得到,比如:

1
2
3
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

addFlags(int flag) 方法的参数只有一个而且类型为 int,而有时需要设置多个 flag,此时就会用到 | 操作服将两个 flag 合并,之所以能够这样操作,是因为 Android SDK 中判断设置的 flag 就是判断的 bit 位。以上面的代码为例,Intent.FLAG_ACTIVITY_CLEAR_TASK 的值为 32768, 二进制为 1000 0000 0000 0000;而Intent.FLAG_ACTIVITY_NEW_TASK 的值为 268435456, 二进制为 0001 0000 0000 0000 0000 0000 0000 0000;所以 Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK 的值为 0001 0000 0000 0000 1000 0000 0000 0000

类似的操作在各种配置判断的情况下都很常见,qt中也有类似的用法