如何在非 UI 线程截图

截图是一个很常见的需求,但网上常见的截图方法都是在主线程运行的。而这有一个隐患,就是卡。因为截图是通过调用 view.draw(canvas),而这就会阻塞主线程的绘制流程引起卡顿。

我司的 App,为了解决卡顿的问题,是在后台线程进行截图的,同样也是调用 view.draw(canvas) 方法,如下方代码所示。第一次看到时挺讶异的,draw 还能在后台线程运行而不报错?介于使用该方法时一直很安全,从未报错,也就默默收起这个疑问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在后台线程调用该方法
public static Bitmap getBitmapByView(LinearLayout linearLayout) {
try {
int h = 0;
Bitmap bitmap = null;
for (int i = 0; i < linearLayout.getChildCount(); i++) {
if (linearLayout.getChildAt(i).getVisibility() == View.VISIBLE) {
h += linearLayout.getChildAt(i).getHeight();
}
}
bitmap = Bitmap.createBitmap(linearLayout.getWidth(), h,
Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
linearLayout.draw(canvas);
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

但没想到之后有一个类似的方法,仅是将参数从 LinearLayout 改为 ScrollView,竟然会偶发性地报错。报错截图如下:

这样看来,draw 是只能在主线程调用了?那之前的方法为什么一直没事呢?

原因

我们先来研究下这个 Exception 是怎么产生的。
从上面的堆栈看出,是由 ViewRootImpl 的 checkThread 引起的。该方法是为了防止我们在非 UI 线程更新 UI。当我们调用 requestLayout、invalidate 时最终都会调用到 checkThread 方法。

那又是如何调用到 checkThread 的呢?首先我们调用了 ScrollView.draw 方法,通过 super.draw 调用了 View.draw 方法。而 Draw 方法中又包含 onDrawScrollBars,它调用了 invalidate 而使得 checkThread 抛出异常。

我们大胆猜测 onDrawScrollBars 是 LinearLayout、ScrollView 调用相同方法却引发不同结果的关键。我们可以把 ScrollView 的 scrollbar 设为 none 来验证一下猜想。果不其然,之后就再不报这个错了。看代码的逻辑,LinearLayout、ScrollView 都会调用 onDrawScrollBars。但由于 LinearLayout 的 scrollbar 为 none 直接返回,而 ScrollView 走进了 onDrawScrollBars 的逻辑引发了 invalidate。

总结

从上面的分析可得,使用 view.draw 后台截图时需要十分小心,不然容易出现类似 ScrollView 的情况而报错。
使用 view.draw 方法截的图和屏幕展示出来的效果是一样的。但某些软件的截图功能两者是不一致的。比如高德地图的路线截图保存功能,屏幕上显示的路线没有完全展开,而截图显示的是完全展开的路线。猜测可能在自定义 View 中写了类似 draw 的方法然后调用,原理应该还是通过 canvas 来实现的。

参考

讨论一下“只能在UI主线程更新View”这件小事
为什么我们可以在非UI线程中更新UI