ZXing BitMatrix 位矩阵全解:一个 int 如何扛起 32 个像素的二维码生成底层逻辑
【免费下载链接】zxingZXing ("Zebra Crossing") barcode scanning library for Java, Android项目地址: https://gitcode.com/gh_mirrors/zx/zxing
你以为的二维码是一堆黑白方块,但在 ZXing 的内存里它只是一段int数组。本文带你钻进BitMatrix的底层逻辑:位压缩是怎么把 200 万个布尔位压成几千个 int 的、一条链接收端二维码生成流程怎么走、以及新手最容易踩的三个坑。全程以"你"的视角展开,读完就能自己动手生成和读取一张码。
扫码偶尔失败、内存悄悄翻倍?先看看链路中间那个 BitMatrix
想象你写的扫码 App:摄像头给出一帧灰度图,最后吐出一个字符串。中间发生过什么?
- 图像先被二值化成一张"黑白矩阵"——每个点只剩黑或白
- 各种 Reader(QRCode、Data Matrix、Aztec……)在这张矩阵上找定位图案、读模块
- 生成端则是反着来:一串字符最终也要落成同样的黑白矩阵,再渲染成 PNG
这张"黑白矩阵"就是 ZXing 里的 BitMatrix。它是整个项目里所有二维码格式的公共容器——扫码失败排查、生成二维码时内存暴涨,多数时候都和它打交道。🔍
位压缩入门:一个 int 抽屉如何装下 32 个像素格子
为什么不用 boolean[][]
一张 300×300 的码有 9 万个点。用boolean[][]存,每个点至少占 1 字节,外加行数组的对象头,开销不小。而 BitMatrix 的"家当"只有四样:
// BitMatrix 的全部内部状态 private int width; private int height; private int rowSize; // 每行占几个 int:(width + 31) / 32 private int[] bits; // 真正的数据,1D 数组9 万个点 ÷ 32 ≈ 2813 个 int,约 11KB。位压缩(bit packing)省下的不是常数,而是 32 倍的量级。📦
一个 int 里住着 32 个"格子"
把一个int想成一个抽屉,里面有 32 个格子,从左到右编号 0~31。每个格子只放一个像素:0 表示白,1 表示黑。更妙的是每行的第一个像素都从新抽屉的第一个格子开始——行与行之间永远对齐,所以整行数据可以整块搬走。
读一个像素,就是"算出哪个抽屉、第几个格子",然后移位取一位:
public boolean get(int x, int y) { int offset = y * rowSize + (x / 32); // 定位到第 y 行、第几个 int return ((bits[offset] >>> (x & 0x1f)) & 1) != 0; // 右移到位,取出 0/1 }写入就是它的镜像:set用|=点亮某个格子,unset用&=熄灭,flip用^=翻转。全部是 CPU 一条指令级别的位运算,没有任何查表。
端到端链路:从 "hello" 到一张 300×300 的码
生成流程:编码 → 纠错 → 填充 → 渲染
QRCodeWriter.encode(...)内部按顺序走完五步(见 QRCodeWriter 源码):
- 编码:字符按选定模式(数字、字母、字节)转成比特流
- 纠错:Reed-Solomon 算法生成冗余码块,码被划破也能读
- 填充:数据按 Z 字路线交错写入模块级矩阵,再叠加掩码图案
- 放大:
renderResult把模块级ByteMatrix按倍率放大,setRegion批量填出每个黑块,四周垫上 4 模块静区 - 渲染:
MatrixToImageWriter逐行读 BitMatrix,true 涂黑、false 留白,写进BufferedImage
解码端:BitMatrix 同样是主角
摄像头帧 →LuminanceSource(灰度)→HybridBinarizer(自适应二值化)→ BinaryBitmap 持有的 BitMatrix。注意它是懒加载的:只有二维 Reader 真正需要时才计算整张矩阵,一维码扫到了就一步都省。所有二维解码器都是在同一张 BitMatrix 上做定位与取样。
上手实操:三步生成 / 读取一张码
生成:三步出图
// 1. 指定像素尺寸与纠错级别 QRCodeWriter writer = new QRCodeWriter(); Map<EncodeHintType, Object> hints = new HashMap<>(); hints.put(EncodeHintType.ERROR_CORRECTION, "M"); // 2. encode 直接返回像素级 BitMatrix(不是模块数!) BitMatrix matrix = writer.encode("hello", BarcodeFormat.QR_CODE, 300, 300, hints); // 3. 渲染成 PNG 文件 MatrixToImageWriter.writeToPath(matrix, "png", Paths.get("hello.png"));读取:解码同样三行
BufferedImage image = ImageIO.read(new File("hello.png")); LuminanceSource source = new BufferedImageLuminanceSource(image); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); Result result = new MultiFormatReader().decode(bitmap); System.out.println(result.getText()); // hello性能与内存:又快又省的关键
内存公式:先算账再建矩阵
内存占用 ≈height × ⌈width/32⌉ × 4字节。300×300 的码只要约 9KB,1000×1000 也才 ~125KB。真正会吃掉内存的往往是渲染阶段的BufferedImage(300×300 的 TYPE_INT_RGB 约 360KB,是矩阵的 40 倍)。所以:按需选择输出尺寸,别为"高清"先建超大矩阵再缩放。
行级操作与扫描提速
- 复用
BitArray:getRow(y, row)传入已有实例可避免逐行重新分配,MatrixToImageWriter就是这么干的 - 先裁后解:
getEnclosingRectangle()返回left, top, width, height,用它对BinaryBitmap.crop(...)后再交给 Reader,处理范围立刻缩小 - 快速探边:
getTopLeftOnBit()/getBottomRightOnBit()两个端点一取,码的包围盒就有了
新手最容易踩的 3 个坑
三个高频错误现场
- 把像素当模块:
encode(…, 300, 300, …)的 300 是像素尺寸,不是 QR 码的模块数。一张 33×33 模块的码放大到 300×300 像素是正常现象,别因为"格子太少"以为码坏了。 - 位序记反,渲染镜像错位:每个 int 里最低位(bit 0)对应更小的 x,且每行从新 int 开始。自己手写渲染或跨 int 拼接时一旦顺序写反,图就是镜像/错位的。对照
get(x, y)的 offset 计算写最稳。 - 复用矩阵忘了 clear:
setRegion/setRow都是就地修改,不会自动擦掉旧位。同一实例连续生成两张码,先matrix.clear(),否则上一张的残影会混进新码。
写在最后
BitMatrix 不挑格式:QR Code、Data Matrix、Aztec 的解码器全在它身上干活。想再深入,可以从rotate90()的行交换实现、toString("X ", " ")的调试可视化入手;文档入口在 docs/index.html。
一句话总结:把 9 万个黑白点装进 2813 个 int,就是 BitMatrix 给条形码与二维码带来的全部魔法。
项目源码:https://gitcode.com/gh_mirrors/zx/zxing
【免费下载链接】zxingZXing ("Zebra Crossing") barcode scanning library for Java, Android项目地址: https://gitcode.com/gh_mirrors/zx/zxing
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考