💡
本篇文章,主要记录关于使用Profile进行Android多线程问题的辅助定位和解决实战。
  • 大纲
      1. 问题现象与背景
      1. 问题初步分析与假设
      1. 问题定位过程(核心)
      1. 根因总结

问题现象与背景

在业务 App “Note” 中,我们有一个通过手势“画圈”快速选中元素的功能,用户无需切换工具即可圈选目标元素。但在实际使用中,偶现圈选后目标元素从界面中消失,但选中框仍然正常显示的问题。
notion image

业务实现背景:

为了区分普通元素绘制与选中状态下的高亮绘制,我们使用了两个 Bitmap 进行绘制:
  • offscreenBitmap:用于绘制所有正常显示的元素;
  • selectedBitmap:用于绘制当前被选中的元素高亮区域;
整体绘制与选中流程如下:
  1. 子线程 将目标元素绘制到 selectedBitmap上(用于 UI 中展示选中框内高亮);
  1. 主线程 将对应元素从 offscreenBitmap中擦除(避免重复显示);
  1. 最后,将选中框 UI 叠加渲染到界面上;
为保证线程安全,上述操作使用了锁机制,确保子线程不会在主线程完成绘制前操作 offscreenBitmap。
结合业务流程与现象(元素消失但选中框存在),初步推测该问题为多线程同步问题导致的数据不一致。

问题定位过程

Profile的Trace抓取功能,能清晰地记录当前每个线程的执行情况,对于多线程问题的定位非常有帮助,可以从调用时序中,清晰看到方法的执行顺序。
为定位问题,使用 Android Studio 提供的 CPU Profiler 工具,通过抓取 Trace 文件,对比 正常情况下与异常情况下的线程执行顺序,寻找差异点。

正常情况 Trace 分析:

  • 主线程按序执行:先绘制目标元素到 offscreenBitmap,然后调用 drawSelectedArea()方法;
  • 子线程由于持有锁,在主线程完成 offscreenBitmap 绘制之后,才开始执行,将对应内容绘制到 selectedBitmap;
  • 整体流程有序,子线程读取到的 offscreenBitmap数据是正确的;
注:图片中,上面的为主线程,下面的为子线程
notion image
notion image

异常情况 Trace 分析:

在多次尝试复现问题后,抓取到了一次典型异常 Trace,发现如下关键差异:
  • 在子线程等待锁期间,主线程提前调用了 eraserSelectedArea()方法,将刚刚绘制到 offscreenBitmap 上的目标元素区域进行了擦除;
  • 之后,子线程才获取到锁,开始执行,从 offscreenBitmap中读取对应区域数据进行绘制;
  • 但由于该区域已经被擦除,子线程实际上读取到的是“空白区域”,最终导致绘制到 selectedBitmap的内容为空;
  • 后续渲染时,选中区域看似选中但实际上没有内容,表现为“元素消失”。
notion image
结合这个错误的流程,我们可以推测问题的发生步骤:
  • 画圈选择时,主线程先绘制选中的元素到offscreenBitmap(这里还是正常的)
  • 此时按照计划,应该要轮到子线程把主线程绘制的内容,绘制到selectedBitmap中,但是,中间却来了个不速之客,eraserSelectedArea将刚刚主线程才绘制的内容,给清空了。
  • 这时候子线程终于拿到锁了,开始执行,浑然不知offscreenBitmap的对应区域已经被清空了,还傻傻从offscreenBitmap中绘制内容到selectedBitmap(但是该区域已经被清空了,当然拿不到内容),此时就会拿到空的内容,导致看起来元素丢失了。
  • 由于子线程并不知道出错了,所以会继续进入后续流程,进而显示选中框。

根因总结

经过对比分析,问题的根本原因在于多线程时序控制不当:
虽然使用了锁来保证线程互斥访问 offscreenBitmap,但关键方法 eraserSelectedArea()的调用时机未被正确约束,导致其在子线程读取数据前,错误地清空了 offscreenBitmap 上的目标区域,造成子线程读取到了无效数据,最终渲染异常。
简而言之:关键操作(擦除)未与绘制/读取操作严格同步,导致数据竞争与状态不一致。

最终解决

既然已经知道是时序问题,导致主线程错误的把选择区域的内容擦除了,这里可以在画圈选择进入选中状态前,直接把主线程的擦除任务移除,避免对应区域被错误擦除。将绘制的逻辑,统一交给画圈选择的流程。
开发记录——WebView播放Youtube在线视频,报错153开发记录:WebView元素拖拽到Android流程记录