/ 今日科技快讯 /
近日,中国空间站阶段飞行任务总指挥部在酒泉卫星发射中心召开神舟十二号载人飞行任务新闻发布会,通报此次任务有关情况。中国载人航天工程办公室主任助理季启明在会上介绍,中国将于2022年完成空间站的在轨建造,建成国家太空实验室。之后,空间站将进入到应用与发展阶段。
/ 作者简介 /
本篇文章来自Youth Lee同学的投稿,和大家分享了WebView白屏检测的相关内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
Youth Lee的博客地址:
https://juejin.cn/user/184373683166734
/ 前言 /
一直有用户反馈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)
/ 白屏处理 /
在检测到白屏之后怎么做呢?
根据实际遇到的情况,我们从运维入手,排查了白屏时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 加载的逻辑都比较复杂,有时候怎么重试也无法成功,这个时候我们会直接降级到加载线上的详情页,优先保证用户的体验。
图文详情页秒开实践:
https://juejin.cn/post/6876011410061852680
我们使用的腾讯的X5,头条是他们自研,可能没有原生的WebView成熟,出现白屏的时候我们还可以降级操作。
最后,如果你知道还有什么白屏原因,希望可以分享一下!
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注