代码笔记 - 从二进制流中以非8bit读取数据

376 阅读4分钟
/**
*@global {Uint8Array} arr - 即字节流如[0xff,0xaa,0x10...]
*@global {Number} pos - 二进制流索引,比如0的时候为这个buffer的二进制第0位
*
*@param {Number} size - 位数,范围可为1~31
*@return {Number}
*/
function read(size) {
    var i, code = 0;
    for (i = 0; i < size; i++) {
        if (arr[pos >> 3] & 1 << (pos & 7)) {
            code |= 1 << i;
        }
        pos++;
    }
    return code;
}

来源 github.com/intellilab/…

说明

这个函数是用来读取那些二进制流中,读取规则并不是8bit 16bit 64bit的数据格式。

典型场景为读取gif数据流做解码时,gif支持自定义位数编码,比如对于黑白二值化的gif图,可以通过用2个bit编码像素,相比8个bit编码像素可以大大减少文件体积。

解析

1. 索引pos右移3位 pos>>3

可以先看看pos>>3的几个结果是什么:

0>>3 //0 
7>>3 //0
8>>3 //1
15>>3 //1
16>>3 //2
255>>3 //31
256>>3 //32

分析计算过程,对于任意二进制,可以表示为十进制计算为:

n_0 \times 2^n + n_1 \times 2^{n-1} + ... +n_n \times 2^{0}

当往右移动三位,即丢弃末尾三位,然后整体除以2^3

n_0 \times 2^{n-3} + n_1 \times 2^{n-1-3} + ... +n_{n-3} \times 2^{3-3}

2^3=8

所以右移之后的结果相当于把一个数除以8之后向下取整,即Math.floor(n/8);

而一个字节刚好是8位二进制。 所以,pos>>3代表的是,在二进制流中,当前位数对应到字节码里面是第几个字节。

2. 索引pos与7 pos & 7

同样,先看看pos & 7的结果:

0 & 7 // 0
1 & 7 // 1
6 & 7 // 6
7 & 7 // 7
8 & 7 // 0
...
63 & 7 //7
64 & 7 //0
65 & 7 //1

可以发现,计算的结果相当于对一个数求8的余数,相当于n%8
分析计算过程,可以把等式化为二进制:

// 0 & 7 = 0
0b00000000 & 0b00000111 //-> 0b00000000

// 1 & 7 = 1
0b00000001 & 0b00000111 //-> 0b00000001

// 63 & 7 = 7
0b00111111 & 0b00000111 //-> 0b00000111

// 64 & 7 = 0
0b01000000 & 0b00000111 //-> 0b00000000

// 65 & 7 = 1
0b01000001 & 0b00000111 //-> 0b00000001

任意数字与7(n & 0b111),都是提取该数二进制模式的末三位;
因此无论所给的数字多大,末三位一定是按顺序在0b0000b111之间,也就是0到7;
因此结果跟求8的余数结果相同。
所以,pos & 7表示的是,当前位数对应到一个字节内第几位

3. 判断值arr[pos >> 3] & 1 << (pos & 7)

由上可知:
arr[pos >> 3]取到的是当前的字节,
pos & 7则是代表当前是字节内的第几位。

因为1的二进制值为0b00000001
根据位运算"与"的特性,任意数字与1(n & 0b00000001),可以提取该数的末位的值;
发生左移时,
左移1位1<<10b00000010, 则可以提取倒数1位的值
左移2位1<<20b00000100, 则可以提取倒数2位的值
以此类推,
arr[pos >> 3] & 1 << (pos & 7)则可以获得索引pos对应的二进制值0或1

4. code |= 1 << i

当索引pos取到的值为1的时候,把值拼到code里面。
for循环之后,就可以拿到具体值。

限制

因为js位运算中左右移运算最大只支持31位,因此参数size的最大只能为31。而且跨字节读取数据还需要考虑大端序小端序的问题。因此最好是使用在小于8bit的场景下。

优点

类型确定,可以避免数字转二进制字符串带来的性能开销。