☰
Android 开发生僻却实用的知识点 Part 3:从二维码性能优化到 Mockito 测试实战
2026/10/10 8:37:03 网站建设 项目流程
  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载

导读

本文是"Android 开发生僻却实用的知识点"系列文章的第三部分,由 WillowTree 团队的 Android 开发者们分享他们在实际项目中踩过的坑与积累的小技巧。文章覆盖七个实战知识点:ZXing 二维码大图生成性能优化、adb命令的小细节、TextUtils.concat()的正确使用、父 View 与子 View 的触摸事件拦截博弈、SQLite 事务的提交/回滚陷阱、单元测试中的静态方法限制,以及用 Mockito 简化对象依赖的实践。读完本文,你将获得可直接复制进工程的高效二维码生成方案、更稳妥的事务与事件分发处理方式,以及一套更清爽的测试代码写法。

如果你是第一次阅读本系列,可以先看看团队在 Part 1(Android Studio Image Asset、StrictMode、adjustResize 等)和 Part 2(WebView 崩溃、RecyclerView position、Collections.emptyList()等)中分享的内容。本文在仓库中的位置是 issue-31/Android开发生僻却实用的知识点Part3.md。


Tidbit One:用 ZXing 高效生成大尺寸二维码

如果你正在使用 ZXing 的二维码生成器库来生成二维码,会发现直接用它生成大图片比较慢。QRCodeWriter.encode()在指定大尺寸时需要对整个矩阵做逐位填充与渲染,尺寸越大耗时越长。

核心思路:先生成最小矩阵,再按需缩放

一个被验证可行的优化办法是:向库传入0 x 0的图片尺寸,此时它会返回一个最小尺寸的BitMatrix(矩阵中每一个块对应 1 像素)。之后你把这个小矩阵写入BitmapDrawable,再将其设为某个 View 的背景,由 View 的绘制系统按需缩放展示——这样就把"生成大图"的昂贵工作,替换成了"生成小图 + 缩放"的廉价操作。

BitMatrix matrix = new QRCodeWriter() .encode("content here", BarcodeFormat.QR_CODE, 0, 0); int height = matrix.getHeight(); int width = matrix.getWidth(); Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { bmp.setPixel(x, y, matrix.get(x, y) ? Color.BLACK : Color.TRANSPARENT); } } BitmapDrawable qrCodeDrawable = new BitmapDrawable(getResources(), bmp); qrCodeDrawable.setFilterBitmap(false); // 关键!防止缩放时模糊 imgQrCode.setBackground(qrCodeDrawable);

三个关键注意点

  1. 必须调用setFilterBitmap(false):如果不关闭位图过滤,BitmapDrawable在放大显示时会被插值平滑,导致二维码的黑白边缘发灰、变得模糊,最终影响扫码识别率。所以在把 Drawable 设进 View 之前,务必先执行qrCodeDrawable.setFilterBitmap(false)。

  2. 按需选择位图格式:原文示例因二维码背景需要透明,使用了Bitmap.Config.ARGB_4444。如果只想要黑白两色、不需要透明背景,可以用Bitmap.Config.RGB_565,内存占用更小。需要提醒的是,ARGB_4444从 API 16 开始已被官方标记为不推荐使用(deprecated),在新项目里建议优先评估RGB_565或直接采用下面的稳妥方案。

  3. 对0 x 0参数的兼容性要保持警惕:库的实现可能在未来版本中不再接受0 x 0这种非常规参数——毕竟二维码的符号结构决定了其最小尺寸也不可能比10 x 10更小。如果你担心升级库后出现兼容问题,可以把创建方法改为显式指定一个较小的尺寸:

BitMatrix matrix = new QRCodeWriter() .encode("content here", BarcodeFormat.QR_CODE, 10, 10);

源码层面的依据

上述 API 行为直接来自 ZXing 的QRCodeWriter与BitMatrix定义:encode(contents, format, width, height)返回用于描述二维码明暗块的矩阵对象,matrix.get(x, y)在坐标为true时表示该块为黑色;而BitmapDrawable的setFilterBitmap(boolean)正是控制缩放时是否进行双线性过滤的开关。这套"小矩阵 + 关闭过滤 + View 背景缩放"的组合拳,是二维码从"生成慢"到"秒出图"的关键。


Tidbit Two:adb hell与adb shell结果相同?

一个看起来匪夷所思、实测却真实存在的小现象:在命令行输入adb hell和输入adb shell得到的结果是一样的——adb会进入设备上的 shell 交互环境。

这个知识点更多是一个有趣的观察记录:adb工具在解析子命令时存在一定的容错行为,因此拼写错误(这里是缺少s的hell)也可能被"温柔地"接受。不过必须强调两点:

  • 不要在脚本或 CI 流程中依赖这种容错行为,它是未文档化的实现细节,任何版本的adb工具更新都可能收紧参数解析规则,让这个"特性"失效;
  • 日常开发请始终书写完整、正确的命令adb shell,避免在自动化场景中埋下不确定性。

Tidbit Three:用TextUtils.concat()优雅地拼接 CharSequence

TextUtils.concat()是 Android 框架层提供的字符串拼接工具:它能把传入的多个CharSequence连接在一起,同时保留它们各自的间距(Spannable 等格式信息),并且返回值仍然是一个CharSequence。

这一点和普通的字符串拼接有本质区别:

  • "a" + "b"会把内容降级为纯String,若源文本携带Spannable(字体、颜色、下划线等富文本样式),拼接过程会丢失这些样式;
  • TextUtils.concat()则保留原CharSequence中的格式数据,适合在需要动态组装富文本(如"用户名 + 时间 + 正文"混合展示)的场景使用。

用法示意:

CharSequence result = TextUtils.concat(title, " - ", timestamp, " - ", body);

它的返回值仍是CharSequence,可以直接传给TextView.setText()或继续参与后续的字符串处理流程,是处理富文本拼接时一个值得记住的 API。

Tidbit Four:父 View 拦截与子 View 的"反拦截"

在 Android 的触摸事件分发机制中,子 View 处理的点击事件通常都是由父 View 先拦截、再分发下来的:事件从ViewGroup的dispatchTouchEvent()进入,经onInterceptTouchEvent()决定是否拦截,最终才到达目标子 View。因此,如果你需要使用某个子 View 正在处理的点击事件,正确做法是使用父 View 的拦截事件方法(onInterceptTouchEvent()返回true)。

但这里存在一个博弈点:子 View 可以通过调用requestDisallowInterceptTouchEvent(true)来禁止父 View 拦截自己正在处理的事件序列。如果父 View 出于业务需要,必须在这种"被禁止"的情况下仍然拦截,那唯一的办法就是重载requestDisallowInterceptTouchEvent()这个方法本身:

@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { // 忽略子 View 的请求,强制保留父 View 的拦截能力 // super.requestDisallowInterceptTouchEvent(false); }

简单总结这套关系:

  • 子 View 想"守住"自己的事件 → 调用requestDisallowInterceptTouchEvent(true);
  • 父 View 想"夺回"事件的拦截权 → 重写requestDisallowInterceptTouchEvent(),不把请求上交给默认实现。

这在实现嵌套滚动(如列表嵌套、滑动冲突处理)时是绕不开的必修课。

Tidbit Five:SQLite 事务的提交/回滚陷阱

在执行批量写入等耗时操作时,调用SQLiteDatabase的beginTransaction()与endTransaction()能显著提升性能(减少磁盘 I/O 次数)。但这里有一个极易踩中的坑:

必须调用setTransactionSuccessful()!

事务的完整流程是:

db.beginTransaction(); try { // 批量 insert / update / delete 操作 db.setTransactionSuccessful(); // 标记事务成功 } finally { db.endTransaction(); // 根据标记决定 commit 或 rollback }

endTransaction()并不会盲目提交:它依据的是此前是否调用了setTransactionSuccessful()。如果你忘了这一行,那么在调用endTransaction()时,本次事务内做出的所有修改都会被回滚——数据库文件看起来"一切正常",但数据实际一条都没写进去,这种静默失败往往比直接报错更难排查。

因此请记住这条铁律:beginTransaction()之后、endTransaction()之前,务必在业务代码正常执行的路径上调用setTransactionSuccessful(),并配合try/finally保证异常情况下也能正确结束事务。

Tidbit Six:单元测试中的静态方法陷阱

如果你正在为应用添加测试用例,千万要小心静态方法:

  1. Android 框架层提供的很多方法在测试单元里根本不能用——它们依赖真实的系统服务、Binder 或底层 native 实现,在 JVM 单元测试(无设备环境)下无法正常执行;
  2. 更麻烦的是,由于它们是静态方法,你甚至不能用 Mockito 之类的框架去模拟它们——Mockito 基于动态代理/子类化实现 mock,只能处理可继承、可覆写的实例方法,静态方法是它的盲区。

这导致一个常见现象:业务代码一旦在关键路径上调用了静态的框架方法(如System.currentTimeMillis()、TextUtils.isEmpty()等),测试代码就会变得难以编写和控制。应对思路包括:

  • 在设计上尽量把静态调用包进可 mock 的实例对象(依赖注入一个"时钟"或"文本工具"包装类);
  • 必要时引入支持静态方法 mock 的专用框架(如 PowerMock 家族),但要注意它自身的兼容性与维护成本。

结合本仓库中的测试主题文章可见,这个约束正是推动"面向接口编程 + 依赖注入"的重要动机之一:可测试性往往倒逼出更好的代码结构。

Tidbit Seven:用 Mockito 简化测试中的对象依赖

与 Tidbit Six 配套的正面建议是:试试 Mockito。它为你在测试用例中创建、配置和验证对象行为提供了简洁的 API,能大大简化之前那些测试用例中复杂的对象依赖——不再需要手写一堆 Fake 或 Stub 类,只需用mock()、when()、verify()等调用即可完成替身对象的装配。

本仓库恰好收录了两篇可直接对照学习的实践文章:

  • 使用Mockito对异步方法进行单元测试:演示了用doAnswer()同步触发回调、以及用ArgumentCaptor捕获回调参数再手动驱动异步逻辑的两种测试方案,解决"异步函数不好测"的经典难题;
doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ((DummyCallback) invocation.getArguments()[0]).onSuccess(results); return null; } }).when(mockDummyCollaborator).doSomethingAsynchronously(any(DummyCallback.class)); // 然后正常调用被测方法并 verify 交互
  • Android测试框架:Dagger2+Espresso2+Mockito:展示如何用Mockito.when(clock.getNow()).thenReturn(new DateTime(2008, 9, 23, 0, 0, 0))让测试日期不再依赖真实时钟,从而断言 UI 显示为"2008-09-23"。

这两个示例分别对应了 Mockito 在"单元测试层"和"UI 测试层"的典型用法,可以作为你落地 Mockito 时的参考实现。


小结

本期的七个知识点看似零散,实则覆盖了 Android 开发的几个高频痛点:

知识点核心要点
ZXing 二维码用0 x 0生成最小矩阵 +setFilterBitmap(false)按需缩放
adb 命令adb hell与adb shell结果一致,但不可依赖容错
TextUtils.concat()保留 CharSequence 样式信息,适合富文本拼接
触摸事件子 View 用requestDisallowInterceptTouchEvent防拦截,父 View 可重写该方法夺回控制权
SQLite 事务忘记setTransactionSuccessful()会导致endTransaction()时全部回滚
静态方法框架静态方法无法在单元测试中执行也无法 mock,需借助设计规避
Mockito用 mock/verify 大幅简化对象依赖,仓库内有异步测试与 UI 测试两份实战参考

这些技巧的共同价值在于:它们都源自真实项目中的具体问题,规模虽小,却能在关键时刻帮你避开一类隐蔽的 Bug 或性能陷阱。更多同系列内容可继续阅读 Part 1 与 Part 2,也欢迎翻阅 issue-31 目录下的其他译文(依赖注入框架、RxJava 调试、应用架构等),扩充你的 Android 知识图谱。

  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载
上一篇:Qwen3-30B-A3B-FP8:中小企业大模型本地化部署的转折点
下一篇:nginx-module-vts高级特性:流量限制、过滤器与实时控制完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询