简介这是一套基于ZXing开源库的二维码扫描Android工程资源面向需要快速集成扫码功能的移动开发者和学习条码识别原理的初学者。资源完整呈现了从摄像头图像捕获、图像预处理到解码输出的核心逻辑并支持QR码、EAN、Code 128等多种码制适合作为开发参考或二次改造的基础。压缩包共1172个文件约4.88MB以java源码、class编译文件、xml配置与布局、json数据为主同时包含gradle、iml等构建工程文件以及ap_资源打包产物目录层次清晰便于按模块理解源码结构和资源索引方式。已有237人学习浏览。包内可见Zxing扫描器的关键实现和资源引用关系能够帮助读者快速定位解码流程、自定义扫描界面及接入方式减少重复开发成本是研究Android端Zxing集成时较为实用的入门资料。1. 二维码扫描 ZXing不是只有拉起扫码页一种玩法扫码这个动作现在已经跟复制粘贴一样常见。但真到自己动手做二维码扫描功能时很多人的第一反应是找现成 SDK或者直接拿 ZXing 的 CaptureActivity 改改就用。ZXing 作为条码识别的老牌开源库确实能干活但大多数人只用了它不到十分之一的能力它不仅能扫二维码还能处理一维条码、反色码、模糊码甚至不依赖相机预览画面直接从 Bitmap 或 YUV 帧里解出内容。这篇笔记会从 ZXing 的解码链路讲起把我实际拆过的扫码模块、参数调整、竖屏适配和性能优化完整过一遍。适合刚接手扫码需求的新手也适合已经被识别率折磨过、想换思路的熟手。2. 为什么是 ZXing条码识别的核心链路与选型逻辑2.1 解码链路的四个环节从 YUV 帧到 DecodeResultZXing 的识别过程不是黑匣子它分四步源数据转换、灰度化、二值化、译码。相机预览回调给到的是 YUVNV21 或 YV12数据ZXing 内部通过 RGBLuminanceSource 把 YUV 分量转成亮度数组这一步相当于灰度图。接着进入二值化环节PlanarYUVLuminanceSource 或 HybridBinarizer 会根据局部阈值算法把灰度图变成黑白图目的是把二维码的深色模块和浅色模块从背景里分离出来。最后是 Decoder 环节MultiFormatReader 会根据 DecodeHintType 里的提示去匹配对应的 Reader比如 QRCodeReader、DataMatrixReader解析出内容并返回 Result。这个链路里最容易被人忽略的是二值化。很多自定义扫码页只改了取景框颜色、扫码音效识别率却上不去问题往往就出在这里。ZXing 默认用 HybridBinarizer它对光照不均的鲁棒性比全局阈值好不少但在极暗或极亮场景下全局阈值GlobalHistogramBinarizer反而更稳。我做过的项目里有台设备在强光下扫码率骤降换成 GlobalHistogramBinarizer 之后识别时间从平均 1.8 秒降到 0.6 秒这个调整只需要改一行代码后面会给出。选 ZXing 而不是自己写识别算法或者用其他商业 SDK主要原因有三点第一它同时支持 QR Code、Data Matrix、Code 128、EAN-13 等二十多种码制扫码枪能读的它基本都能解第二纯 Java 实现不依赖 NDK集成成本低出问题可以跟代码第三ZXing 的解码逻辑是分离的理论上可以把整个识别引擎当作一个纯函数来用——传入像素数组传出结果。这个特性是后面做离屏扫码和帧直通解码的基础也是它跟那些封装死页面的付费 SDK 最大的区别。2.2 选型边界什么时候不推荐 ZXingZXing 不是万能的。它有两个明显短板复杂背景下的定位能力和超低分辨率码的识别。如果二维码印刷在金属拉丝表面、带反光的包装袋上或者背景里存在大量近似方块图案的干扰ZXing 的 Finder Pattern 定位会失效抛出 NotFoundException。这时换用 OpenCV 做预处理的方案更实际比如先做透视校正、膨胀腐蚀再把处理后的 Bitmap 丢给 ZXing 解。另一个场景是二维码太密比如一张 2cm 见方的码里塞了 200 个字符摄像头分辨率不够时 ZXing 解不动需要提高预览分辨率或建议用户凑近。还有一个容易踩的边界ZXing 不支持识别屏幕上的动态码或者说识别率极低。因为它默认假设码是静态的帧与帧之间没有做时序融合。如果你要做「扫码枪对准屏幕上的码」这类场景需要自己处理连续帧的置信度累加或者干脆用专门做屏幕码识别的方案。另外ZXing 的 Java 版性能上限大约在每帧 80ms 左右的解码耗时720p 预览中端设备如果项目对实时性要求高于 30ms 一帧就得考虑 OpenCV 的 QRCodeDetector 或者商业库。不过对大多数扫码登录、扫码支付、码上开柜这类场景ZXing 完全够用。选型结论是标准码制、常规光照、静态码ZXing 是性价比最高的选择极端场景再考虑叠加其他库。3. 集成落地从依赖引入到自定义扫码页的完整流程3.1 依赖引入与核心类职责我一般不用 ZXing 官方 Android 仓库里那个打包好的 coreandroid 模块因为里面塞了太多用不到的东西比如摄像头配置、UI 组件、历史记录。我更习惯只引 core自己控制相机和 UI。这样体积小、逻辑透明出问题也好排查。implementation com.google.zxing:core:3.5.2如果你需要扫描一维码记得加上这个判断MapDecodeHintType, Object hints new EnumMap(DecodeHintType.class); hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE)); Reader reader new MultiFormatReader(); reader.setHints(hints);核心类就三个MultiFormatReader 负责按 hints 分派到具体的 ReaderRGBLuminanceSource 接收像素数据BinaryBitmap 承载二值化结果。其余像 CaptureActivity、ViewfinderView 这类 UI 类完全可以不引。这样你拿到的是「解码引擎」而不是一套别人画好的页面。3.2 自定义扫码页相机预览与取景框裁剪很多人在这里翻车直接拿系统相机预览整帧去解结果发现识别率极低。原因是二维码在画面中占比小ZXing 对整帧解码时背景噪声干扰了定位。正确做法是把取景框区域的图像裁剪出来再做解码。我常用的实现是从 TextureView 或 SurfaceView 拿帧然后按取景框坐标裁剪// 从 ImageReader 拿到 YUV 数据后先裁剪取景框区域再解码 byte[] nv21 imageReader.acquireLatestImage().getPlanes()[0].getBuffer().array(); Rect cropRect getCropRect(previewWidth, previewHeight, viewfinderWidth, viewfinderHeight); PlanarYUVLuminanceSource source new PlanarYUVLuminanceSource( nv21, previewWidth, previewHeight, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), false); BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); Result result multiFormatReader.decodeWithState(bitmap);这里有两个关键参数。第一个是 cropRect 的坐标原点它对应的是相机传感器方向不是屏幕方向。竖屏预览时 YUV 帧是横向的即宽大于高所以裁剪坐标必须做一次旋转映射。第二个参数是 PlanarYUVLuminanceSource 构造方法里的最后一位 reverseHorizontal当摄像头是前置时需要设 true否则镜像会导致定位失败。这几个参数错一个表现就是「取景框明明框住了码但就是扫不出来」。这个坑后面避坑章节还会专门展开。3.3 连续扫码的循环与频率控制扫码模块做完单帧识别还不够实际应用里需要连续扫码。我的做法是固定 200ms 间隔取帧解码而不是每帧都解。因为解码是 CPU 密集型操作每帧都解会让手机发热而且帧与帧之间结果高度重复没有意义。private final Handler handler new Handler(Looper.getMainLooper()); private final Runnable decodeRunnable new Runnable() { Override public void run() { if (isDecoding) return; isDecoding true; try { Result result decodeFrame(); if (result ! null) { onScanned(result.getText(), result.getBarcodeFormat()); return; } } catch (Exception e) { // NotFoundException 属于正常忽略 } finally { isDecoding false; handler.postDelayed(this, 200); } } };decodeFrame 里做的是第 3.2 节那套裁剪解码逻辑。200ms 间隔怎么来的人的手稳程度和相机的自动对焦周期都在这个量级太快了没意义太慢了会让取景框看起来「卡」。如果你扫码的码是小码或远距离码建议改成 100ms 并配合 TRY_HARDER hint近距离大码可以放到 300ms省电。3.4 自动对焦与缩放策略自动对焦不是 ZXing 负责的事但它是识别率高低的决定性因素之一。我见过太多项目漏掉这个Camera 打开后没设置对焦模式结果近处码糊成一片识别率不到 30%。Camera.Parameters params camera.getParameters(); ListString focusModes params.getSupportedFocusModes(); if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); } else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); camera.autoFocus(new Camera.AutoFocusCallback() { Override public void onAutoFocus(boolean success, Camera camera) { // 对焦失败不重试靠下一次触发 } }); } camera.setParameters(params);这里有个细节容易忽略连续对焦模式CONTINUOUS_PICTURE在部分设备上只对「拍照」场景生效预览场景可能不工作。遇到设备对焦来回抽风的情况我一般会改成触屏对焦点击取景框后触发 autoFocus锁定焦点。另外提示一下Zoom 别动——很多设备在变焦后预览尺寸会变YUV 帧的宽高会和 ImageReader 配置的不一致直接导致解码数组越界。我一般固定 1.0x 缩放让用户物理靠近或远离来对焦。4. 常见问题与避坑识别慢、反色码、竖屏错位、内存抖动4.1 现象识别慢扫一个码要 2 秒以上原因往往不是 ZXing 算法慢而是送入解码的帧太大。默认相机预览分辨率可能是 1920x1080这么大的帧做二值化和定位耗时直接翻倍。另外没有裁剪整帧解码也会导致算法在背景区域浪费计算。解决把预览分辨率降到 1280x720并严格裁剪取景框区域。实测 720p 裁剪后的解码耗时比 1080p 整帧快了约 2.5 倍。如果还是慢把 TRY_HARDER hint 去掉它在某些场景下会显著增加计算量。4.2 现象深色背景上的浅色二维码反色码扫不出来ZXing 默认假设二维码是深色模块印在浅色基底上。遇到反色的码比如白码黑底找 Finder Pattern 时会直接失败。解决把图像反色后再送进去。不需要额外引依赖ZXing core 里没有提供反色工具类我在实践中是在拿到 Bitmap 后手动做像素反转int[] pixels new int[width * height]; bitmap.getPixels(pixels, 0, width, 0, 0, width, height); for (int i 0; i pixels.length; i) { int color pixels[i]; int r 255 - Color.red(color); int g 255 - Color.green(color); int b 255 - Color.blue(color); pixels[i] Color.rgb(r, g, b); } Bitmap inverted Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);然后把这个 Bitmap 转成 RGBLuminanceSource 再解码。注意 Bitmap 必须是 ARGB_8888 格式RGB_565 下 Color.red 取不到正确值。实际项目里这个策略极大提升了支付宝付款码等反色码的识别率。加一个自动检测策略第一次解码失败后自动换反色再试一次对用户体验来说这不增加感知延迟但成功率有明显提升。4.3 现象竖屏扫码时取景框和实际识别区域错位原因相机传感器的原生方向是横向的竖屏 UI 下 YUV 帧需要旋转 90 度。如果旋转信息没同步到 cropRect 的计算里裁剪的区域就会错位。解决建立坐标系映射函数。我一般维护一个 orientation 变量拍照方向竖屏时是 90然后对裁剪矩形做如下转换Rect getCameraCropRect(int previewWidth, int previewHeight, int viewfinderWidth, int viewfinderHeight) { int rotatedWidth, rotatedHeight; if (getResources().getConfiguration().orientation Configuration.ORIENTATION_PORTRAIT) { rotatedWidth previewHeight; rotatedHeight previewWidth; } else { rotatedWidth previewWidth; rotatedHeight previewHeight; } // 以屏幕 UI 取景框左上方为原点计算 int left (rotatedWidth - viewfinderWidth) / 2; int top (rotatedHeight - viewfinderHeight) / 2; // 映射回传感器坐标系竖屏时 left 对应传感器 toptop 对应传感器宽减去 right return new Rect(top, left, top viewfinderHeight, left viewfinderWidth); }这个映射关系是固定套路但每家设备的 Camera 驱动可能略有不同。我在测试时会打印 previewWidth 和 cropRect 的实际值配合一张带网格的测试图来验证。如果格子对上说明映射没问题对不上就微调。4.4 现象扫码过程中内存抖动频繁 GC帧率下降原因每帧都创建 PlanarYUVLuminanceSource 和 Bitmap解码完就丢给 GC。Android 的 GC 在这种高频小对象分配压力下会频繁触发造成卡顿。解决复用对象和缓冲区。把 PlanarYUVLuminanceSource 声明为成员变量decodeFrame 时调用其构造方法更新内部引用而不是 new 新对象。推荐的做法是直接复用同一个 source 实例并保存 YUV 数组的引用避免分配新的 byte 数组if (source null) { source new PlanarYUVLuminanceSource(nv21, previewWidth, previewHeight, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), false); } source.setCropRect(cropRect.left, cropRect.top, cropRect.width(), cropRect.height()); // 注意setCropRect 在 ZXing 3.5.x 中已提供旧版本需要 new 新对象这里提示一下ZXing 的 PlanarYUVLuminanceSource 在 3.5.x 版本里支持更新 crop 区域如果是 3.3.x 或更早版本每次还是需要 new。建议升级到 3.5.2性能和新特性都有改善。还有一个容易被忽略的点ImageReader 的 acquireLatestImage 每次都会产生新的 Image 对象用完必须 close否则 buffer 会累积内存只涨不降。我遇到过线上扫码页面连续扫码 20 分钟后 OOM最后定位到就是 Image 没 close。4.5 现象第一次扫码正常第二次起识别率骤降原因MultiFormatReader 的 decodeWithState 方法会保留上次解码的状态包括 Reader 内部的临时变量。如果上次解码抛了异常状态没有重置第二次就会直接失败。解决每次解码前调用 reset或者干脆每次 new MultiFormatReader。我实测 new 的开销很小比 reset 更保险MultiFormatReader reader new MultiFormatReader(); reader.setHints(hints); try { Result result reader.decodeWithState(bitmap); } finally { reader.reset(); }注意 decodeWithState 和 decode 的区别decodeWithState 允许复用 hint 状态但异常后状态脏decode 每次会重新应用 hints安全性更高。如果你用 decodeWithState务必在 finally 里 reset——这是我踩过的真实坑上线后扫码成功率在首扫 100%、二扫 40% 之间徘徊排查了大半天才抓到。5. 进阶技巧相机帧直通解码与离屏扫码把 ZXing 用成引擎而不是页面到这里ZXing 的基础用法已经够用了。但真正把它的价值榨干是绕过 Camera 预览这个环节直接解码任意来源的图像。离屏扫码的核心是只要你能拿到像素数据ZXing 就能解不管它来自相机、截图、相册还是网络图片。我先说个场景。某公司做了一个批量盘点系统需要扫描几百个固定资产上的二维码扫码枪成本高手机连扫又累。最后我实现了一个离屏批量识别工具用户拿手机对着表格拍一张照一张表里有 5x315 个码程序把这张照片切成 15 个子图分别丢给 ZXing 解一次性拿到全部编号。这个需求如果只用扫码页要操作 15 次用离屏解码一次拍照就完成。具体技术路径是读取 Bitmap用 Bitmap.createBitmap 按行列切块每块 300x300 像素以上保证解码成功率然后逐块解码。我一般是按网格切但实际应用中二维码可能歪斜所以推荐先用灰度化加边缘检测粗定位再把候选区域扩大 20% 防止切歪。ZXing 对旋转的容忍度大约在 ±15 度以内超过就得预处理旋转。另一个实用的进阶技巧是「扫码成功音效延迟验证」。相机解码有时会误识别——比如把背景中的某块图案当成码解出错误的字符串却不多见。我习惯在返回结果前加一道校验对解出的文本做格式正则校验比如 URL 必须以 http 开头、纯数字编号必须是指定长度。这一步在离屏批量识别时尤其重要一张图里 15 个码解错一个后续溯源很麻烦。要是你想更极致可以不经过 Bitmap直接把 network image 下载后的 byte[] 喂给 RGBLuminanceSource。省去 Bitmap 的中间层内存占用和耗时都更低。但这样就没法做旋转预处理了你自己评估风险。最后分享一个我的个人习惯从那以后我每次接入 ZXing 都会强制走一遍「裁剪-复用-重置」这个流程——裁剪取景框复用解码对象每次解码后 reset。这样一个模块基本不会出现识别率低、内存涨、连续扫码失败的玄学问题。另外凡是涉及反色码、横竖屏切换的场景我都在自动化测试里加了两张基准图片一张正常二维码、一张反色二维码每轮改动都跑一遍防止回归。希望帮到你。本文还有配套的精品资源点击获取