后端开发第一次碰到位掩码

· 约 2 分钟读完

0 1 2 3 4 和 0 1 2 4 8 16 32

后端开发定义枚举,习惯性地从 0 开始往上加:

public enum DeviceType {
    TEMP(0),
    HUMIDITY(1),
    LIGHT(2),
    PRESSURE(3),
    MOTOR(4);
}

对接固件协议的时候,看到的值却是这样的:

{
  "hwInstalled": 19,
  "hwEnabled": 17,
  "hwHealth": 1
}

19 是什么意思?怎么不是个数组?

因为固件那边不是在编号,而是在占位。每个设备占一个二进制位:

设备十进制值
温度传感器bit 01
湿度传感器bit 12
光照传感器bit 24
气压传感器bit 38
电机控制器bit 416

19 的二进制是 10011,第 0 位、第 1 位、第 4 位是 1,代表”装了温度传感器、湿度传感器和电机控制器”。

为什么不老老实实用数组

后端要表示”设备上装了温度、湿度、电机三个模块”,大概会这样写:

{
  "hwInstalled": ["TEMP", "HUMIDITY", "MOTOR"]
}

固件用一个整数就搞定了:1 + 2 + 16 = 19

一个 32 位整数能塞 32 个开关,64 位能塞 64 个。不需要数组,不需要序列化,一次内存读取就拿到所有状态。在嵌入式场景里,内存和带宽都很紧张,这种表示方式非常自然。

怎么读:按位与

拿到一个 hwInstalled = 19,想知道电机控制器在不在里面:

long installed = 19;
long MOTOR = 16; // 即 1 << 4

if ((installed & MOTOR) != 0) {
    // 电机控制器存在
}

& 是按位与,两个数的同一位都是 1 结果才是 1。19 & 16

10011   (19)
10000   (16)
-----
10000   (16,非零,说明电机控制器是在的)

换一个,查光照传感器(bit 2 = 4):

long LIGHT = 4;

if ((installed & LIGHT) != 0) {
    // 光照传感器存在
}

19 & 4

10011   (19)
00100   (4)
-----
00000   (0,光照传感器不在)

怎么写:按位或、异或、取反

设置一个位用 |(按位或):

long flags = 0;
flags |= (1L << 0); // 开温度传感器,flags = 1
flags |= (1L << 4); // 开电机控制器,flags = 17

清除一个位用 & ~(按位与 + 取反):

flags &= ~(1L << 4); // 关电机控制器,flags 回到 1

翻转一个位用 ^(异或):

flags ^= (1L << 4); // 电机开着就关,关着就开

一次解出多个设备

实际对接时,拿到一个整数要把所有置位的设备都解出来。遍历每一位就行:

String[] names = {"TEMP", "HUMIDITY", "LIGHT", "PRESSURE", "MOTOR"};
long installed = 19;

for (int i = 0; i < names.length; i++) {
    if ((installed & (1L << i)) != 0) {
        System.out.println(names[i] + " 已安装");
    }
}

输出:

TEMP 已安装
HUMIDITY 已安装
MOTOR 已安装

三个字段,同一套位定义

固件协议里经常会用同一套位定义、配上不同语义的字段。比如硬件模块可能有三个状态字段:

字段含义
hwInstalled物理上装了哪些模块
hwEnabled当前启用了哪些
hwHealth哪些工作正常

三个值做一次按位与,就能找出”装了、启用了、而且工作正常”的模块:

long working = installed & enabled & health;

如果某个模块 installed 里有但 health 里没有,说明硬件在但出了故障。这种判断一行位运算就够了,不需要三个数组做交集。

后端对接时的注意点

用 Java / Kotlin 对接这类协议,有几个容易踩的地方:

  • long 不用 int。固件协议里的位掩码经常超过 32 位,int 只有 32 位会溢出。字段声明和字面量都要用 long / 1L
  • 位号从 0 开始1 << 0 是第一个设备,不是 1 << 1
  • << 优先级低于比较运算符flags & 1 << 5 实际上是 flags & (1 << 5)——这个恰好没问题,但 flags & 1 << 5 != 0 会被解析成 flags & (1 << (5 != 0)),所以位运算一定要加括号:(flags & (1L << 5)) != 0