
简介这是一份面向安卓开发者的二维码与条形码扫描实现示例基于ZXing开源库配套完整可运行工程与说明文档。资源共1874个文件压缩包18.42MB主要包含Java源码、安卓资源文件、构建配置及APK成品其中.java、.xml、.gradle文件便于查看集成逻辑.jar与.dex文件是库依赖和编译产物可对照实际运行效果学习。已有2535人浏览学习。压缩包内含MyApplication示例应用演示了从Gradle依赖引入、自定义扫描界面、相机预览到解码结果回调的完整流程附带《说明使用文档.docx》对集成步骤、常见权限问题和扫码体验优化做了整理。读者可通过阅读源码与文档快速掌握在Android项目中集成ZXing、处理扫描结果以及加入闪光灯和扫描区域指示等功能适合移动开发入门或需要在项目中落地扫码功能的工程师。1. 把扫码能力接进项目zxing 解码库的真实边界很多 App 的需求清单里二维码扫描和条形码扫描看着不起眼真做起来才知道牵扯到预览、图像格式、解码策略一堆细节。我最早接触 zxing是被拉去给一个识别率极低的内部工具“擦屁股”——那套方案对图片二值化处理不干净稍微有点反光就罢工后来把解析层换成 zxing 的 MultiFormatReader问题才压住。zxing 能做的是从摄像头帧或静态图片中定位条码、做旋转矫正、二值化和解码兼容 QR Code、Code 128、EAN-13、UPC-A 等常见码制并返回条码内容和格式。它不负责相机预览 UI更不包含商品信息查询。它的最佳受众是客户端的扫码登录/扫码支付开发者、服务端批量解析条码的后端以及想快速验证识别率的测试工程师。2. 不急着接代码zxing 的解码链路与依赖边界2.1 从像素到结果二值化、定位、解码在干什么扫码识别的复杂度不在“读出来”这一步而在“怎么从真实世界的图像里把码分离出来”。zxing 的处理管线大致是亮度源 → 二值化 → 定位 → 解码。第一步是提取 LuminanceSource亮度源。zxing 不关心色彩只要每个像素的灰度亮度。手机摄像头拍照出来的是 YUV、NV21 或 RGBA 数据直接用 Y 分量亮度做输入是最省内存的做法。如果拿 Bitmap则通过 getPixels 取到像素数组后做一次灰度转换。第二步是二值化。真实环境下光照不均匀同一个码上半部分亮、下半部分在阴影里很常见。如果全图用同一个阈值黑色模块会被一刀切成两半解码自然失败。所以 HybridBinarizer 把图像切成 8x8 的子块逐块计算亮度统计值找出局部阈值再把相邻块的阈值做平滑。大多数手机扫码场景都用它。GlobalHistogramBinarizer 更快但对光照波动敏感适合可控光线的服务端图片。第三步是定位。zxing 需要在二值图上找到条码区域。QR Code 靠三个角上的“回”字形定位图案锁定方向线性条码则依赖边缘梯度和黑白条纹宽度的比例关系做扫描线搜索。第四步是解码。对应每一种格式有专门的 Reader 实现比如 QRCodeReader 会按版本和纠错等级去还原数据块Code128Reader 根据起始符和终止符推算信息。MultiFormatReader 会在内部把支持的 Reader 挨个尝试直到某一种解码成功或全部失败。真正需要设计的地方就在这里多格式同时解码会让你省事但也会带来不必要的耗时。把这套链路理解清楚后面所有参数和坑都有了解释不会有“加个开关就好”的玄学。2.2 依赖怎么选core、javase、android 模块差异zxing 项目分多个模块我只说常用的三条路径。客户端只要解码能力依赖core包自己做相机预览。相机帧数据的采集、旋转、裁剪由 CameraX 或 Camera2 处理zxing 只负责从帧里找码。一般写法是把帧转成 PlanarYUVLuminanceSource再交给 HybridBinarizer 和 MultiFormatReader 去解码。桌面端或服务端批量识别图片依赖javase包。它提供了 BufferedImageLuminanceSource可以直接读java.awt.image.BufferedImage同时带了命令行工具能一次跑一个目录下的所有图片。对批量生产图片模拟测试非常实用。至于 zxing 的 android 模块很多老项目还在用它把 Camera、取景框、解码 Activity 都封装好了。如果你接手的是一个历史包袱重的项目可能还会见到CaptureActivity这种类但如果这是新项目我更建议只引 core 配合自有 UI 来做。原因很简单android 模块的设备适配逻辑停更很久现代机型摄像头输出格式差异大它封装的 Camera 接口未必能处理最后还是要自己改。依赖模块典型使用场景需要自己准备的部分coreAndroid 客户端实时识别相机预览、取帧、旋转、UIjavase服务端/桌面批量识别图片JVM 运行环境、图片路径处理android老项目扫码页面快速改造相机权限、生命周期适配android-integration跳转外部扫码 App外部 App 的安装与兼容性2.3 版本与运行环境匹配别盲目升新版我常用的做法是根据最低系统版本定库版本。zxing core 3.5.x 分支对 JDK 版本有要求如果服务端还在 Java 8选太新的版本可能编译不过Android 项目把 minSdk 提到 21 以上基本顺畅minSdk 低于 19 时会遇到 API 不支持的风险。这里给不出一个“万能版本”但可以反向判断在依赖配置里写死大版本号锁一个稳定小版本不要在项目交付前顺手改版本——我吃过一次亏升级后解码耗时波动变大最后查出来是新版本里的多格式解码线程配置不同导致。提示版本升级后最少要做一轮真机回归不要只跑单元测试。扫码这种功能模拟器上通过不代表实拍也能过尤其是低端机的图像采集延迟和设备旋转角度。2.4 调优的最关键入口DecodeHintType 参数zxing 的解码行为不是写死的通过DecodeHintType可以显著改变识别率和速度。这几个参数我几乎每次都用参数作用我的建议POSSIBLE_FORMATS限定只尝试哪些码制能多省就多省全格式解码会拖慢速度TRY_HARDER用更细的扫描线、更多轮次去定位弱光或条码破损时打开平时关掉CHARACTER_SET指定解码后的字符串编码中文二维码要设 UTF-8ALSO_INVERTED允许对反色条码白码黑底解码外贸单据和部分产品包装会用到ALLOWED_EAN_EXTENSIONSEAN/UPC 附加码的位数书本条码常见按需开启值得多说一句POSSIBLE_FORMATS不是越多越好。同一帧图像里如果同时开了 QR、Data Matrix、PDF417、Code 128MultiFormatReader 会逐个尝试每多一种格式耗时就多一截。我平时的习惯是业务上明确只扫二维码就只传BarcodeFormat.QR_CODE需要兼容一维码时再补CODE_128和EAN_13。全格式解码只留在调试模式里用上线前务必收紧。3. 把 zxing 接进项目CameraX 取帧与解码逻辑落地3.1 取到 Y 平面别拿到 Bitmap 再转换先看依赖。项目里除了 zxing core还需要摄像头取帧的库。CameraX 是现代 Android 项目里比较稳妥的选择它把 Camera2 的细节封得比较干净ImageAnalysis 可以直接拿到 YUV 帧。implementation(androidx.camera:camera-core:1.3.0) implementation(androidx.camera:camera-camera2:1.3.0) implementation(androidx.camera:camera-lifecycle:1.3.0) implementation(com.google.zxing:core:3.5.2)依赖就这四个。注意到我刻意没有引 zxing 的 android 模块因为用 CameraX 自己取帧控制力更强。CameraX 的 ImageAnalysis 每一帧都是一份ImageProxy里面包含三个图像平面Y、U、V。解码只需要亮度也就是 Y 平面。Y 平面并不是一个简单的ByteArray(width * height)。它的rowStride可能大于图像宽度因为摄像头硬件会做行对齐每行末尾补几个字节的 padding。如果直接用buffer.remaining()取全部字节亮度源行尾会串入 padding 数据图案错位识别率直接掉一截。正确做法是逐行复制fun extractYArray(imageProxy: ImageProxy): ByteArray { val width imageProxy.width val height imageProxy.height val yPlane imageProxy.planes[0] val yBuffer yPlane.buffer val rowStride yPlane.rowStride val yArray ByteArray(width * height) var row 0 var offset 0 while (row height) { yBuffer.position(row * rowStride) yBuffer.get(yArray, offset, width) offset width row } return yArray }这段代码是在做“去 padding”的操作。rowStride是每一行在内存里实际占用的字节数width是有效像素宽度二者不一定相等。逐行把有效数据拷贝进目标数组后面 zxing 拿到的亮度源就是干净平整的。如果设备支持也可以直接用ImageProxy.PlaneProxy的rowStride做整图映射但逐行复制是最不容易踩坑的思路性能开销在解码面前可以忽略。3.2 分析器里构造亮度源和解码器拿到 Y 数组之后下一步是构造 zxing 需要的数据结构。这里有个常见误区直接把 Y 数组当成RGBLuminanceSource来用。RGBLuminanceSource接收的是 RGBA 像素数组传 Y 数据进去等于让解码器把灰度值当成颜色通道去解读结果永远不对。YUV 帧必须用PlanarYUVLuminanceSource。class ScannerAnalyzer : ImageAnalysis.Analyzer { private val hints mapOf( DecodeHintType.POSSIBLE_FORMATS to listOf( BarcodeFormat.QR_CODE, BarcodeFormat.CODE_128, BarcodeFormat.EAN_13 ), DecodeHintType.TRY_HARDER to true, DecodeHintType.CHARACTER_SET to UTF-8 ) private val reader MultiFormatReader().apply { setHints(hints) } private var onResult: ((String, String) - Unit)? null override fun analyze(imageProxy: ImageProxy) { val width imageProxy.width val height imageProxy.height val yArray extractYArray(imageProxy) val source PlanarYUVLuminanceSource( yArray, width, height, 0, 0, width, height, false ) val bitmap BinaryBitmap(HybridBinarizer(source)) try { val result reader.decodeWithState(bitmap) onResult?.invoke(result.text, result.barcodeFormat.name) } catch (_: NotFoundException) { // 当前帧没找到条码继续预览不打断 } catch (_: ChecksumException) { // 定位到了但校验失败常见于遮挡或角度太大 } finally { imageProxy.close() } } }核心逻辑就是三步Y 数组 → 亮度源 → 二值图 → 解码。PlanarYUVLuminanceSource构造函数里的六个数字分别是yArrayData, width, height, left, top, cropWidth, cropHeight, reverseHorizontal。这里的left/top是 0先做全图解码。decodeWithState会复用MultiFormatReader内部状态比每次都 new 一个 Reader 省分配。imageProxy.close()必须放在 finally 里漏掉的话 CameraX 会一直认为帧还在处理中画面逐渐变卡。需要注意MultiFormatReader不是线程安全的。如果同一个ScannerAnalyzer实例在多个执行线程里跑解析会出现奇怪的崩溃我一般会给解码方法加一个synchronized或者直接保证ImageAnalysis使用单线程 Executor。3.3 绑定预览并处理结果回调分析器写好后需要在页面里把Preview和ImageAnalysis都绑定到同一个ProcessCameraProvider上。val analysis ImageAnalysis.Builder() .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysis.setAnalyzer(Executors.newSingleThreadExecutor(), scannerAnalyzer) cameraProvider.bindToLifecycle( lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis )STRATEGY_KEEP_ONLY_LATEST很关键。它表示分析器处理不过来时直接丢弃中间帧只处理最新帧。这样解码慢一点只会降低识别帧率不会让预览越来越卡。如果选STRATEGY_BLOCK_PRODUCER相机采集会被阻塞画面直接掉帧。另一个点OUTPUT_IMAGE_FORMAT_YUV_420_888要显式指定否则不同设备默认输出格式不一致有的会给你 RGBA分析代码就得写两套分支。结果回调到 UI 层后还要做防抖。不然一个码被连续识别几十次扫码页面会抖得像抽风private var lastScanAt 0L private fun onScanResult(text: String, format: String) { val now System.currentTimeMillis() if (now - lastScanAt 2000L) { return } lastScanAt now viewModel.onScanCompleted(text, format) }两秒阈值是我常用的经验值。识别完成后如果业务需要继续扫下一个码过两秒再恢复解析如果不需要可以在拿到结果后直接解绑分析器避免后台继续耗电。许多项目在这里偷懒结果扫码完成后相机还在偷偷逐帧分析发热和内存问题都从这儿来。4. 扫码实战中的避坑与排查四条最典型的现场问题4.1 竖屏场景识别率低到怀疑人生现象手机明明是竖着拿的取景框里二维码显示也正常但识别率低到 10% 以下尤其是一维条码几乎全灭。横屏旋转手机反而能扫出来。原因摄像头传感器是横着安装的CameraX 输出的 Y 平面数据方向并不等于手机屏幕方向。zxing 拿到的亮度源是传感器原始方向二维码有定位图案转几个角度还能勉强识别Code 128、EAN-13 这类线性条码对方向极其敏感角度不对就完全解不出来。解决在构造亮度源之后根据imageProxy.imageInfo.rotationDegrees对亮度源做旋转。常见做法是判断 90、180、270 三个旋转角对应调用rotateCounterClockwise()。注意这个方法会重新拷贝一次图像内存放到解码前的 try 块里就行不用单独缓存。我一般会写一个函数把旋转逻辑包起来避免每个分析器里都堆一段 if-else。注意有些设备rotationDegrees会返回 0 或 180但预览画面依然是正的这是因为 CameraX 在预览路径上已经做了坐标系转换。分析器拿到的帧不一定和预览方向一致一定要以ImageInfo.rotationDegrees为准不要以预览显示效果为准。4.2 弱光环境下连续翻车现象暗光场景下画面里明明能看到二维码轮廓但识别过程反复失败偶尔成功一次要卡好几秒。同样的代码在白天自然光下非常流畅。原因光线不足时传感器会提高增益来补偿曝光噪点被一并放大。HybridBinarizer 的局部阈值对这种噪点图稳定性有限黑色模块边缘会被噪点打断导致定位失败。解决两条路并行。第一条是产品层面加手电筒按钮让用户主动补光这是市面上大多数扫码 App 的做法第二条是代码层面开TRY_HARDER它会用更密集的扫描线去搜索识别率有提升但耗时也会增加。我一般还会顺便检验一下图像质量先算 Y 数组的平均亮度低于某个阈值就直接跳过本帧不执行解码。这个阈值我习惯定在 64 到 96 之间太亮也不行全是白板的时候解码纯属浪费 CPU。val avgBrightness yArray.average().toInt() if (avgBrightness 64) { imageProxy.close() return }这段代码放在构造亮度源之后可以有效过滤掉过暗的帧。平均亮度的计算成本不高但能避免大量无效解码。所谓“扫码暗光翻车”多半不是 zxing 不行而是把每帧都拿去硬啃实际该放弃的帧没放弃。4.3 某些条码反复扫不出来现象同一个二维码相册里的截图扫出来了摄像头对着屏幕扫不出来换一台手机却能扫出来。或者 Code 128 条码打出来的标签上面能扫下面那截扫不出。原因原因有好几层按概率排序。第一POSSIBLE_FORMATS没把这种格式加进去第二二维码在画面里占比太小zxing 定位完发现面积不够直接放弃第三条码被产品包装的曲面遮挡边缘有断裂第四商家自己生成的条码质量差模块边缘不整齐。解决先在POSSIBLE_FORMATS里补上对应格式这是最容易被忽略的一步。如果格式没问题试着把取景框裁剪到画面中间区域只对 ROI 做解码。ROI 区域越小条码在解码图里的相对占比越大识别率会明显提升。做法是改PlanarYUVLuminanceSource的left/top/cropWidth/cropHeight参数。裁剪的时候ROI 的宽度和高度最好是偶数部分设备的 YUV 数据对奇数宽度会报错。如果是对着屏幕扫码还有一个常见问题屏幕本身会闪烁产生明暗条纹。解决思路是把分析器的曝光时间调长一点或者用硬件层面的电频同步但这一般较难做到。我实际更常用的是让用户把手机稍微拉远一点减少摩尔纹干扰。4.4 内存抖动与日志刷屏现象扫码页面打开 5 分钟内存涨得很明显Logcat 里不断出现ArrayIndexOutOfBoundsException或者大量NotFoundException堆栈。原因ImageAnalysis的每一帧都是一次内存分配Y 数组、亮度源、二值图全部要建对象。如果imageProxy.close()没有执行缓存帧不断堆积。NotFoundException本身是正常的——哪一帧没有码都会抛这个异常但有些项目直接在分析器里打了Log.e导致 logcat 几十条几百条地刷。解决第一imageProxy.close()放 finally第二NotFoundException和ChecksumException属于“预期内失败”不需要写日志第三控制分配频率。MultiFormatReader.decodeWithState内部会复用 Reader 的对象池但我们的 Y 数组和亮度源是每次都要新建的这部分省不掉。更好的办法是降低分析帧率比如ImageAnalysis只处理每两帧中的一帧。实验数据上解码成功率并不会明显下降内存和电量的改善却很直接。5. 进阶从相册选图解析与反向生成二维码5.1 相册选图的解码路径除了摄像头实时识别很多业务场景还需要从相册里选一张图片解析比如截图里的收款码、文档里的条形码。静态图片的解码和实时帧不同它没有 YUV 数据拿到手的是 Bitmap。这时要把像素数组喂给RGBLuminanceSource。fun decodeImage(bitmap: Bitmap): Result? { val width bitmap.width val height bitmap.height val pixels IntArray(width * height) bitmap.getPixels(pixels, 0, width, 0, 0, width, height) val source RGBLuminanceSource(width, height, pixels) val bitmapImage BinaryBitmap(HybridBinarizer(source)) return MultiFormatReader().apply { setHints(mapOf( DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE), DecodeHintType.TRY_HARDER to true )) }.decode(bitmapImage) }这段代码和摄像头路径的主要区别在亮度源。RGBLuminanceSource内部的像素格式必须是 RGBA_8888所以bitmap.getPixels之前先确认 Bitmap 的配置是 ARGB_8888。如果源头是黑白图或 RGB_565转一下格式再做不然像素值解析会混乱。相册图片有可能很大解码前先做压缩把长边压到 1024 左右再解析速度会快很多识别率也不会降。5.2 生成二维码把BitMatrix画成图很多时候扫码功能是双向的——不仅要读码还要展示一个二维码给别人扫。zxing 的生成接口藏在MultiFormatWriter里它返回一个BitMatrix也就是一个布尔矩阵需要自己把它画到 Bitmap 上。fun createQrBitmap(content: String, size: Int 512): Bitmap { val bitMatrix MultiFormatWriter().encode( content, BarcodeFormat.QR_CODE, size, size ) val bitmap Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) for (x in 0 until size) { for (y in 0 until size) { val color if (bitMatrix[x, y]) Color.BLACK else Color.WHITE bitmap.setPixel(x, y, color) } } return bitmap }参数上size指的是输出图像的像素宽高不是二维码实际模块数。zxing 会根据内容和容量决定二维码的版本也就是模块数量然后在传入的size范围内自动缩放。如果内容很长比如带了一长串 URL生成出来的二维码会有很多小模块建议增大size避免扫描失败。反向生成时还有个坑默认生成的 BitMatrix 二维码四周边距很窄有些扫码工具会识别失败。解决方式是手动加白边把BitMatrix绘制到更大的白色 Bitmap 中间四周各留 8 到 16 像素。另外逻辑上生成二维码时最好在 UI 线程外操作。encode过程对长文本内容耗时可能在几十毫秒到几百毫秒之间如果在主线程直接调用页面会掉帧。直接用Executors.newSingleThreadExecutor()包一层即可。扫码功能做久了我养成一个习惯每次接入新项目都会先列一个验证清单摄像头实时解码、相册静态图解码、批量生成后再解码三条链路分开测。那次因为只在真机上验证了实时扫码、没测相册路径结果图片解析漏了RGBLuminanceSource的格式转换上线当天就被用户翻到相册扫码的问题。从那以后每条链路我都会强制走一遍尤其是“生成→解码→对比原文”的闭环能少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取