前言
一直有用户反馈APP中WebView页面白屏,包括自己也遇到了几次。关于如何检测拍屏,去网上浏览一番,自己整理了一套方案。大体分为一下几步:
- 截取当前屏幕的内容,获得Bitmap
- 判断Bitmap是否为白色图片
- 针对白屏做相应的处理
WebView截屏
截屏的时机
WebView有以下函数:
public void setWebViewClient(WebViewClient client) {}
入参 android.webkit.WebViewClient
中有以下函数:
/**
* Notify the host application that a page has finished loading. This method
* is called only for main frame. Receiving an {@code onPageFinished()} callback does not
* guarantee that the next frame drawn by WebView will reflect the state of the DOM at this
* point. In order to be notified that the current DOM state is ready to be rendered, request a
* visual state callback with {@link WebView#postVisualStateCallback} and wait for the supplied
* callback to be triggered.
*
* @param view The WebView that is initiating the callback.
* @param url The url of the page.
*/
public void onPageFinished(WebView view, String url) {}
我们在 onPageFinished
里准备截图即可,代码如下:
//应该是x5 SDK有bug,导致onPageFinished 进度100 时会回调两次,这里做个缓存
private final ArrayList<String> completedPageCache = new ArrayList<>();
@Override
public void onPageFinished(WebView webView, String url) {
//会出现多次进入onPageFinished的情况,最后一次进度为100
if (completedPageCache.contains(url)) return;
//进度为100
if (webView.getProgress() > 99) {
completedPageCache.add(url);
//开始截图
new WebWhiteChecker(WebActivity.this, webView, url).startCheck();
}
super.onPageFinished(webView, url);
}
onPageFinished
实际会调用多次,每次进度都不一样,但是最后进度为100。webView.getProgress() > 99
这里保证进度达到100才开始截图。
completedPageCache
用来存储已经做过截图的网页地址。因为我们使用的是腾讯X5 SDK,实际中会遇到进度为100的情况调用两次。这里做个去重,真是坑啊!
X5 WebView的类名跟原生得几乎一致,上面的相关的api都是通用的。
开始截图
直接上代码:
fun startCheck() {
webView?.postDelayed({
try {
//Activity不处于被销毁的状态
if (!activity.isDestroyed && !activity.isFinishing) {
webView.x5WebViewExtension?.let {
//这里取一半大小,不然可能OOM
val bitmap = Bitmap.createBitmap(webView.width / 2, webView.height / 2, Bitmap.Config.ARGB_8888)
//这里必须设置0.5f, 跟上方bitmap的缩放比例一致,不然无法截图
it.snapshotVisible(bitmap, false, false, false, false, 0.5f, 0.5f) {
//开始检测
checkOnSubThread(bitmap)
}
}
}
} catch (e: Exception) {
L.e(e)
}
}, 1000)
}
postDelayed
设置了1秒的延时,是因为APP有的网页全是图片,图片没加载出来时,背景是白色的,没有延时会导致截取白色背景(1秒是自己实验出来的,实际可以设置大一点)。
createBitmap
时宽高选择了WebView的一半,因为之前使用原始大小产生了OOM,尴尬!另外也可以提升接下来的扫描效率,毕竟量小了嘛。
snapshotVisible
是X5 SDK提供的截图方法,简单好用!原生WebView怎么截图,大家可以自行搜索!我还没试过~
Bitmap检测
Bitmap有个getPixel
函数:
/**
* Returns the {@link Color} at the specified location. Throws an exception
* if x or y are out of bounds (negative or >= to the width or height
* respectively). The returned color is a non-premultiplied ARGB value in
* the {@link ColorSpace.Named#SRGB sRGB} color space.
*
* @param x The x coordinate (0...width-1) of the pixel to return
* @param y The y coordinate (0...height-1) of the pixel to return
* @return The argb {@link Color} at the specified coordinate
* @throws IllegalArgumentException if x, y exceed the bitmap's bounds
* @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
*/
@ColorInt
public int getPixel(int x, int y) {
checkRecycled("Can't call getPixel() on a recycled bitmap");
checkHardware("unable to getPixel(), "
+ "pixel access is not supported on Config#HARDWARE bitmaps");
checkPixelAccess(x, y);
return nativeGetPixel(mNativePtr, x, y);
}
这个方法返回了 a non-premultiplied ARGB value
, 恕我能力有限,不知道这个是啥,我有猜测但是我不能瞎说呀!有知道的大神可以指导一下。
throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
如果Bitmap是HARDWARE类型的会抛异常。HARDWARE类型可自行搜索了解一下。
有关x, y传参,直接看下面调用:
//白点计数
private var whitePixelCount = 0
private fun checkOnSubThread(bitmap: Bitmap) {
//异步线程执行
RxSchedulers.scheduleWorkerIo {
val width = bitmap.width
val height = bitmap.height
for (x in 0 until width) {
for (y in 0 until height) {
if (bitmap.getPixel(x, y) == -1) {//表示是白色
whitePixelCount++
}
}
}
if (whitePixelCount > 0) {
val rate = whitePixelCount * 100f / width / height
//这里可以对比设定的上限,然后做处理
}
bitmap.recycle()
}
}
首先要保证检测过程要在子线程执行,不然你懂的!
我们把Bitmap的宽高理解成二维数组,直接两层循环往getPixel
传入下x(with),y(height).这样就可以取出每个像素点的色值了。
bitmap.getPixel(x, y) == -1//表示是白色
为啥-1表示白色,这个我真不知道😓。。。专业知识不够,只能剑走偏锋,自己搞个啥都没有的网页测试出来的(举一反三,其他色值也可以测试出来具体的值)。
其实你把 Color.WHITE
打印出来也是 -1。当然严格来说,这并不能证明 getPixel
获取的-1就代表白色!
每判定一次白色,whitePixelCount
就加一,最后除以Bitmap的宽*高,得到白色的占比。
咱们搞个微软的必应 https://cn.bing.com/
来试试 :
检测结果如下:
WebWhiteChecker: white rate = 4.73251
实际应用中,我们APP设置的阈值时95%,也就是说白色占比超过阈值就认为是白屏。
深色模式下的情况有待研究,实际测试下来虽然背景是黑色的,但是白色占比竟然跟正常模式是一样的!
补充
其实Bitmap中还有一个函数:
@NonNull
public Color getColor(int x, int y) {
checkRecycled("Can't call getColor() on a recycled bitmap");
checkHardware("unable to getColor(), "
+ "pixel access is not supported on Config#HARDWARE bitmaps");
checkPixelAccess(x, y);
final ColorSpace cs = getColorSpace();
if (cs.equals(ColorSpace.get(ColorSpace.Named.SRGB))) {
return Color.valueOf(nativeGetPixel(mNativePtr, x, y));
}
// The returned value is in kRGBA_F16_SkColorType, which is packed as
// four half-floats, r,g,b,a.
long rgba = nativeGetColor(mNativePtr, x, y);
float r = Half.toFloat((short) ((rgba >> 0) & 0xffff));
float g = Half.toFloat((short) ((rgba >> 16) & 0xffff));
float b = Half.toFloat((short) ((rgba >> 32) & 0xffff));
float a = Half.toFloat((short) ((rgba >> 48) & 0xffff));
// Skia may draw outside of the numerical range of the colorSpace.
// Clamp to get an expected value.
return Color.valueOf(clamp(r, cs, 0), clamp(g, cs, 1), clamp(b, cs, 2), a, cs);
}
直接返回一个android.graphics.Color
对象,为什么不使用这个函数呢?
- 因为
RequiresApi
限制,看下图:
em...需要Android Q才行
- 因为它比
getPixel
多了一层将色值转化成Color对象的过程,涉及到全图二维数组的遍历,效率上肯定差了!
好奇的朋友肯定会打印getColor
在白色情况下获得的Color对象,我这里先贴出来:
Color(1.0, 1.0, 1.0, 1.0, sRGB IEC61966-2.1)
em...看不太懂,应该是白色吧
白屏处理
在检测到白屏之后怎么做呢?
根据实际遇到的情况,我们从运维入手,排查了白屏时WebView的请求情况,发现资源接口返回了204--No Content 没有新文档,浏览器应该继续显示原来的文档。
而我们WebView设置使用的是默认缓存策略:
// 设置缓存模式
WebSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
也就说204的时候我们使用了本地缓存,猜测使用缓存的时候出现了问题。
所以我们APP在包含WebView的页面退出之后,执行了清除WebView缓存的工作:
public static void clearDataAndCache(@NonNull WebView webView) {
//清除缓存
webView.clearCache(true);
webView.clearFormData();
webView.clearHistory();
}
经证实清除缓存的操作在大部分情况下都是有效的。
那么关于白屏的原因到底是什么,可以参考这篇今日头条品质优化 - 图文详情页秒开实践中所说:
而在 Android 中,我们采用的是自研内核 WebView,也会遇到一些奇奇怪怪的坑。
多线程读模板文件问题,WebView 在运行中会读取的文件模板,如果此时另外一个线程同时更新模板文件时,就出现了模板加载问题,所以需要保证模板加载的原子性 Render 卡死问题,内核是一个比较复杂的逻辑,内部渲染极少数情况也会出现 Render 卡死问题,但是在详情页整体用户的量级下,即使只有十万分之一的可能,对用户来说也是一个比较大的问题,此时我们会从业务上做白屏监控进行重试
当然不管是 iOS 和 Android, WebView 加载的逻辑都比较复杂,有时候怎么重试也无法成功,这个时候我们会直接降级到加载线上的详情页,优先保证用户的体验。
我们使用的腾讯的X5,头条是他们自研,可能没有原生的WebView成熟,出现白屏的时候我们还可以降级操作。
最后,如果你知道还有什么白屏原因,希望可以分享一下!