尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android生成微信可扫二维码:ZXing核心原理与踩坑实战

Android生成微信可扫二维码:ZXing核心原理与踩坑实战 去年做活动报名项目时遇到一个很常见但又特别折腾的需求用户在 App 里填写完报名信息页面上要弹出一个二维码现场工作人员用微信扫一下就能核销。第一版做出来之后问题不断——用测试机微信扫码有时候完全没反应有时候扫出来是一串乱码换到暗色壁纸之后同一张二维码又扫不出来了。后来排掉这些坑才发现二维码生成不难难的是生成一张在各种环境下都能被微信稳定识别的码。这篇把安卓端生成可用微信扫码的二维码这件事从头到尾讲透包括方案取舍、二维码编码的核心原理、ZXing 核心库的使用方式、几个真实踩坑案例的排查链路以及如何做样式优化而不牺牲识别率。适合刚开始做二维码开发的 Android 工程师也适合那些已经能出图、但总被反馈扫不了的开发者。1. 为什么要在 App 端直接生成二维码先搞清方案取舍1.1 服务端生成和客户端生成怎么选二维码生成最常见的两种路子一个是服务端生成图片下发另一个是客户端本地生成。很多团队默认选服务端理由是后端提供接口返回图片URL前端只管展示看起来干净。但实际落地时会发现问题不少每次展示都要网络请求弱网环境体验很差如果每张二维码内容都不一样后端要额外维护模板和存储要临时拼个本地场景的码比如离线活动签到服务端方案就傻了。当时报名场景里每张二维码的内容都带不同的活动ID和用户ID如果走服务端生成我需要额外在服务器维护存储和失效逻辑还要保障高并发下的请求响应。最后直接改成客户端生成App 只拿到一个字符串参数本地拼好内容用 ZXing 在内存里画出 Bitmap响应速度比网络请求快一个量级离线也能用。1.2 客户端生成适合哪些场景举几个我实际遇到过的合适场景本地数据动态生成比如存储在手机本地、没上服务器的优惠券、核销凭证、入场票用客户端生成二维码再展示给别人扫数据不出本机逻辑也简单。内容不能上送服务器的场景有些隐私信息只希望一次性展示给对方扫描不希望经过后端中转本地生成更合适。批量生成比如需要一次生成几十张带不同参数的二维码客户端本地循环生成即可完全不依赖接口响应速度。离线环境无网情况下也要能出码的场景客户端生成是唯一选择。不适合的场景也要说清楚如果二维码内容必须包含登录态签名而且签名算法不能暴露在客户端里那就必须放到服务端做签名再生成客户端生成会把密钥暴露出去这在安全合规上是大忌。2. 二维码成像的原理搞懂这几件事才能避开微信扫不出的坑2.1 二维码不是简单的黑白格子QR 码快速响应码的生成过程本质上是一个把数据变成二进制位流、再映射到二维矩阵里的编码过程。用大白话说它要经过这几步数据分析系统先判断内容类型。如果是纯数字用数字模式编码如果是字母数字用字母数字模式如果是中文或特殊符号走字节模式或汉字模式。不同类型的数据编码压缩效率不一样。数据编码把字符转成二进制位流。纠错编码用 Reed-Solomon 算法生成纠错码字。这一步是二维码能容错的关键当二维码被遮挡、污损、或者人为覆盖了一部分Logo时扫码工具依然可以从残余数据里把原始内容恢复出来。矩阵布置把数据位流和纠错位流按规则填充到一个矩阵中同时绘制三个位置探测图形就是二维码三个角上的回字形方块、校正图形、时序图形等辅助定位信息。掩码操作对矩阵里的数据区域做掩码处理让黑白模块分布尽量均匀避免出现大片黑色或大片白色的区域。格式信息把纠错级别、掩码编号等信息写入固定区域方便扫码端快速解析。这些流程对开发者来说不需要自己实现但理解它就能解释很多人遇到的坑为什么 Logo 盖上就扫不出来因为 Logo 干扰了数据区域为什么白边太小扫不出来因为扫码端要靠位置探测图形来定位没有安静区就难以识别边界。2.2 纠错级别加Logo之前先看清这个参数QR 码有四个纠错等级L、M、Q、H分别能恢复约 7%、15%、25%、30% 的数据损失。级别越高内部模块数越多、图案越密抗遮挡能力越强但同样内容的二维码视觉上会更挤。很多人做带 Logo 的二维码时直接用默认的 M 级别然后在中间盖一个挺大的 Logo结果微信一扫就失败。原因很简单默认纠错级别只恢复了约 15% 的数据冗余而Logo可能一下占掉了 20% 甚至更多的模块区域数据恢复不出来扫码就失败了。我加 Logo 至少用 Q 级保险起见用 H 级Logo 的尺寸控制在二维码总尺寸的 10% 到 15%不要超过四分之一。这个比例是反复测试出来的最大兼容性和美观度的平衡点。2.3 白边安静区是二维码的呼吸空间QR 规范要求二维码四周留有一定宽度的空白区域叫安静区quiet zone正常要求至少 4 个模块宽度。如果二维码紧贴边缘、或者旁边有其他图案干扰扫码工具就难以定位位置探测图形识别率直线下降。在 ZXing 生成时EncodeHintType.MARGIN这个参数控制白边宽度。很多人为了好看会把这个值改成 1 甚至 0结果就是在微信扫码时没反应。我实测下来MARGIN 保持在 2 以上比较安全4 是规范推荐的稳妥值。尤其当二维码要用于打印、喷绘、或者隔着一段距离扫码时白边一定要给足。3. 技术选型我为什么选 ZXing core 而不是整套依赖3.1 主流方案对比Android 生成二维码的主流库其实没太多选择空间ZXing老牌、稳定、社区活跃支持 QR 码、DataMatrix、Code128 等多种码制。它既能生成也能识别核心库纯净依赖不重是我最终选的方案。ZBar识别性能不错但维护频率比 ZXing 低生成能力也弱一些一般只做扫码端使用。自研生成器不建议。QR 码规范本身不复杂但细节特别多比如版本选择、掩码优化、纠错码计算、格式信息填充自己实现容易踩到兼容性坑投入产出比太低。第三方商业 SDK有些人会引入微信开放平台 SDK 或友商 SDK 来扫一扫但那些主要是做扫码识别不是生成而且集成更重。生成场景不推荐。3.2 只引入 core 模块别引 javaseZXing 仓库里分了很多模块Android 项目只需要core就够了。很多新手会直接引入com.google.zxing:javase因为网上搜索到的示例大多是 Java SE 环境的代码里用了BufferedImage。这在 Android 环境里跑起来会直接抛NoClassDefFoundError因为 Android 里没有java.awt.image.BufferedImage这个类。正确做法是只引 coreimplementation com.google.zxing:core:3.5.2版本我用的 3.5.2比 3.4.1 更新的版本。核心 API 基本没变用起来很稳。如果要更老的兼容性3.4.1 也没问题但 3.5.2 修复了一些内部实现的小问题建议直接用新版本。4. 完整代码实现四步跑通二维码生成4.1 封装一个生成工具类先给一个可以直接用的工具类。这里面做了两件事一是通过 ZXing 的MultiFormatWriter把字符串转成BitMatrix二是把BitMatrix转成 Android 的Bitmap。public class QRCodeUtils { /** * 生成二维码Bitmap * * param content 二维码内容 * param size 图片边长像素 * return 二维码Bitmap失败返回null */ public static Bitmap generateQRCode(String content, int size) { return generateQRCode(content, size, ErrorCorrectionLevel.M, 1); } public static Bitmap generateQRCode(String content, int size, ErrorCorrectionLevel level, int margin) { MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.ERROR_CORRECTION, level); hints.put(EncodeHintType.MARGIN, margin); try { MultiFormatWriter writer new MultiFormatWriter(); BitMatrix matrix writer.encode(content, BarcodeFormat.QR_CODE, size, size, hints); int[] pixels new int[size * size]; for (int y 0; y size; y) { int offset y * size; for (int x 0; x size; x) { pixels[offset x] matrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF; } } Bitmap bitmap Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); bitmap.setPixels(pixels, 0, size, 0, 0, size, size); return bitmap; } catch (WriterException e) { e.printStackTrace(); return null; } } }这里有个关键点我必须强调EncodeHintType.CHARACTER_SET设置为 UTF-8这个直接决定了中文内容能不能被正确编码。ZXing 3.x 默认就是 UTF-8但为了兼容编码环境差异我还是建议显式设置。EncodeHintType.ERROR_CORRECTION默认用 M足够大多数普通场景使用EncodeHintType.MARGIN控制白边默认值我给的 1但正式发布时建议根据场景调整后面排查章节会细说。4.2 BitMatrix 到 Bitmap 的关键细节BitMatrix只是保存了二维坐标上的布尔值不是可直接显示的图像。你需要自己遍历矩阵生成 ARGB 像素数组。这段代码有几个细节创建 Bitmap 用ARGB_8888不要用RGB_565。后者没有透明度通道颜色会有偏差而且在某些设备上显示二维码时对比度不够。像素数组长度是 size * size必须先初始化不然后面赋值容易数组越界。二维码是前景黑色、背景白色所以判断为 true 时填0xFF000000纯黑不透明false 时填0xFFFFFFFF纯白不透明。这里不要填反反了就是黑底白码微信识别率会降低后面案例细讲。4.3 在界面里使用Activity 或 Fragment 里的调用方式很简单Bitmap qrBitmap QRCodeUtils.generateQRCode( https://example.com/activity/123, 480, ErrorCorrectionLevel.M, 2 ); if (qrBitmap ! null) { imageView.setImageBitmap(qrBitmap); }这里的 480 是边长兼顾了屏幕展示和喷印需求。不要用 100x100 的小尺寸然后让 ImageView 拉伸放大拉伸会产生锯齿和模糊微信识别率会明显下降。如果二维码要用于打印建议生成尺寸至少 600x600打印出来才清晰。4.4 批量生成时要注意线程MultiFormatWriter本身编码很快但如果列表页一次性生成几十张二维码每一张都要遍历像素数组合起来也可能卡顿。批量生成时放到子线程ExecutorService executor Executors.newSingleThreadExecutor(); executor.execute(() - { Bitmap bitmap QRCodeUtils.generateQRCode(content, 480); runOnUiThread(() - { if (bitmap ! null) { imageView.setImageBitmap(bitmap); } }); });使用 Bitmap 完成后记得在合适时机调用bitmap.recycle()或让引用置空避免内存压力过大。特别是列表页频繁刷新时不回收很容易 OOM。5. 微信扫码识别不了的完整排查链路这部分是我这次最想写的。下面的案例都是我实际踩过的按排查过程来呈现。5.1 案例一中文内容扫出来变成乱码症状生成内容是活动签到张三微信扫出来是一堆奇怪的字符。排查过程最开始以为是微信版本问题换了几台手机都一样。用 ZXing 官方在线解码页面zxing.org/w/decode.jspx去解同一个 Bitmap 导出的图片发现解码结果就是乱码。这说明问题出在生成端不是扫码端。根因编码时用的字符集和微信解码时不一致。如果不显式指定 UTF-8某些库或旧版本会默认用 ISO-8859-1单字节编码中文字符被拆成多个单字节微信再按 UTF-8 解码就变成乱码。解决生成时强制设置EncodeHintType.CHARACTER_SET为 UTF-8。改完后再用 ZXing 在线解码验证乱码问题消失。经验如果二维码内容是 URL建议直接用完整 URL 字符串生成不要对中文做额外的 URL 编码。因为有些扫码入口会对结果再做一次解码双重编码反而容易出问题。实测直接用中文 URL 生成微信也能正常识别并跳转。5.2 案例二加了 Logo 之后十个有八个扫不出来症状不加 Logo 时二维码一切正常在中间加一个品牌 Logo 之后微信经常扫不出来或者要扫好几次才成功。排查过程一开始以为是 Logo 画的位置问题调整了位置还是不行。后来把纠错级别从 M 改成 H问题一下子就缓解了。再继续缩小 Logo 尺寸到 15% 以内稳定率接近 100%。根因纠错级别 M 只恢复 15% 的数据损失而 Logo 区域覆盖了不小的模块区域。当 Logo 面积超过容错能力时扫码端无法恢复完整内容。Logo 越大需要的纠错能力越强。解决加 Logo 前把纠错级别调到 H30%Logo 尺寸控制在总尺寸的 10%~15%。绘制 Logo 时建议给 Logo 加一个白色圆角底框或白色描边让 Logo 区域与二维码模块形成视觉隔离既美观又能在扫码时减少干扰。public static Bitmap addLogo(Bitmap src, Bitmap logo, float ratio) { int width src.getWidth(); int height src.getHeight(); int logoSize (int) (Math.min(width, height) * ratio); Bitmap combined Bitmap.createBitmap(width, height, src.getConfig()); Canvas canvas new Canvas(combined); canvas.drawBitmap(src, 0, 0, null); int left (width - logoSize) / 2; int top (height - logoSize) / 2; Paint paint new Paint(Paint.ANTI_ALIAS_FLAG); canvas.drawBitmap(logo, null, new Rect(left, top, left logoSize, top logoSize), paint); return combined; }5.3 案例三白边太小、尺寸太小时微信完全没反应症状二维码在手机屏幕上清晰可见但微信扫了毫无反应既不报错也不跳转。排查过程这个案例最迷惑。解码工具能解出来内容也没问题但微信就是不识别。后来把生成尺寸从 100x100 提高到 480x480再把 MARGIN 从 1 加到 4微信立刻就能扫出来了。根因两个问题叠加。第一100x100 的二维码每个模块对应的像素太少边界模糊微信这种对图像质量敏感的扫码工具容易失败。第二MARGIN1 时几乎等于没有安静区位置探测图形的边界特征不明显扫码端定位困难。解决MARGIN 设置 2 或 4尺寸至少 300x300推荐 480~600。实测同样内容MARGIN1 时微信识别距离明显变短MARGIN4 时扫得最顺畅。二维码用于打印时安静区更重要。补充还有一个比较隐蔽的问题——展示背景颜色。有一次二维码背景是浅灰色壁纸白色模块和背景区分不开微信也扫不出来。二维码展示区域务必保证纯白底或者至少与白色模块有足够亮度差。5.4 案例四反色/彩色二维码的兼容性问题症状设计师要求黑底白码的酷炫效果或者白底蓝码。结果微信扫不出来或者偶尔能扫出来但识别很慢。排查过程直接用 MARGIN4、尺寸 480、UTF-8 编码、H 级纠错排除了其他变量问题依然存在。基本可以断定是配色问题。根因微信扫码识别依赖模块之间的明暗对比。反色二维码黑底白码在某些扫码工具里能识别但微信的算法对反色支持不稳定尤其是在暗光环境下几乎无法识别。彩色二维码则往往因为前景色和背景色亮度太接近模块边界不清晰。解决优先使用标准黑底白码。如果一定要做主题色保持深色前景色如暗红、深蓝、墨绿 浅色背景白、浅灰、浅黄并在生成前对比前景和背景的灰度差。最稳妥的做法是二维码主体保持黑白标准配色只把 Logo 区域做成品牌色这样既有视觉亮点又不牺牲识别率。5.5 排查清单照着查就行检查项推荐值说明CHARACTER_SETUTF-8中文内容必须显式设置ERROR_CORRECTIONM普通/ H带LogoLogo 遮挡必须 HMARGIN2~4打印场景给 4生成尺寸至少 300x300推荐 480~600背景纯白或浅色避免浅灰/浅色壁纸前景/背景对比度灰度差尽量大不要用反色Logo 面积不超过 15%使用圆形/圆角白底内容长度尽量短越长模块越密识别越难6. 进阶优化把二维码做得又好看又能扫6.1 自定义颜色的实现原理如果确实要自定义二维码颜色核心要点是前景用深色背景用浅色并保证灰度差足够大。实现上改造工具类的像素赋值部分把写死的0xFF000000和0xFFFFFFFF替换成可传入的颜色public static Bitmap generateQRCode(String content, int size, int foregroundColor, int backgroundColor) { MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 2); try { MultiFormatWriter writer new MultiFormatWriter(); BitMatrix matrix writer.encode(content, BarcodeFormat.QR_CODE, size, size, hints); int[] pixels new int[size * size]; for (int y 0; y size; y) { int offset y * size; for (int x 0; x size; x) { pixels[offset x] matrix.get(x, y) ? foregroundColor : backgroundColor; } } Bitmap bitmap Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); bitmap.setPixels(pixels, 0, size, 0, 0, size, size); return bitmap; } catch (WriterException e) { e.printStackTrace(); return null; } }代码本身不复杂但有两个经验三个位置探测图形三个角上的回字形一定要保持深色因为扫码端靠它们定位。如果为了美观把探测图形也做成浅色识别基本会失败。颜色方案做之前先在纸上算一下灰度差。灰度值 0.299R 0.587G 0.114B前背景灰度差低于 100 时识别率会明显下降就别选了。6.2 关于样式二维码的一点看法现在市面上有一些艺术二维码工具能把二维码嵌到插画里确实很好看。但对商用项目我建议优先可靠性。如果必须做艺术二维码至少遵守三条底线不动三个定位角、不大幅收缩安静区、不在数据区域添加大面积装饰性元素。样式做好后用不同品牌的手机、在不同环境光下实测多轮把扫码成功率当成上线前的必测项而不是只截一张图发给设计师看。6.3 性能和内存的注意点生成尺寸不要无限放大480x480 已经足够清晰再往上就是内存浪费。一个 480x480 的 ARGB_8888 位图大约占 0.9MB1000x1000 要占 4MB列表页并发生成时很容易 OOM。如果同一个页面临时用很多次二维码建议用 LruCache 做一层缓存避免频繁创建和回收 Bitmap。7. 个人踩坑总结二维码生成这事本身不难难在生成的二维码在各种设备、各种环境、各种扫码工具下都能被稳定识别。我自己的经验是最常踩的还是白边、编码和 Logo 这三个坑每个坑背后都有对应的原理。另外分享一个开发时的小技巧拿到生成的 Bitmap 后先导出图片用 ZXing 官方在线解码工具zxing.org/w/decode.jspx验证一下内容是否与预期一致。如果解码结果正确但微信扫不出来问题大概率出在扫码端兼容性或环境条件如果解码就已经不对那就是生成端编码有问题。这个判断能帮你快速定位问题方向省掉不少无效排查时间。
返回列表