##什么是“白屏时间”
白屏时间是指用户输入网站地址到浏览器开始显示内容的时间。当用户打开一个链接或者在浏览器输入一个地址开始访问的时候,就开始等待页面的展示,页面渲染的时间越短,用户等待的时间就越短,用户的感知越好,减少用户的跳出,这样可以极大的提升用户的体验。需要注意的是白屏并不是特指屏幕为白色,"白屏"也有可能是黑色的,重点是用户等待的时间过长,或者页面异常导致没有有效的页面,此类情况,我们都称之为"白屏"。
页面的白屏情况有以下几种:
(1)首屏加载前的白屏。
(2)页面代码崩溃导致的页面无法加载的白屏。
(3)资源代码错误导致spa无法渲染白屏。
##白屏产生过程
白屏时间是页面展示过程中的一部分,所以就涉及到了一个老生常谈的问题———从浏览器输入地址到页面展示的过程,
![image.png](image/knowledge/a2aa8f5b-0b77-45d2-975a-299b6be665d6.png)
页面从url到展示过程
(1)DNS查询,域名解析,获取服务器的IP。
(2)建立TCP链接。
(3)客户端发送HTTP请求。
(4)服务器响应请求。
(5)浏览器解析并渲染页面。
我们可以很清楚的看到页面的整个加载过程,从输入url后到页面的渲染展示过程中的等待时间, 我们就视为"白屏时间",此类的白屏时间归类为正常的白屏,因为最终的结果是页面可以渲染出来的。对于这个时间的加载优化需要极为重视,因为性能问题是多种多样的。情况好点的,网站和应用程序产生一些微不足道的延迟,这些延迟会给用户一些不好的交互体验。也有极其糟糕的情况,那就是它们完全无法访问,对用户输入没有反应,或两者兼而有之。很多不利网站访问速度的因素会形成累加,从而严重影响网站的性能,导致网站访问速度变慢,用户体验低下,最终导致用户流失。
# 异常"白屏"
1)异常白屏产生场景
表现是浏览器无法查看有效页面,但是产生的这种情况的原因则不相同,正常的白屏为我们可以查看到页面加载完毕的结果,但是异常导致的白屏则不能,异常的白屏主要是发生在HTTP响应和浏览器渲染过程。
2)白屏原因
异常类的白屏可能是资源,或代码异常导致的页面白屏,这种情况下,我们正常检测白屏的代码可能还未加载,所以这时候无法使用正常的白屏方案检测,需要通过后端模拟浏览器访问,查看是否白屏。图1.4为通过后端检测出来的白屏,这类情况因为资源加载错误或js报错引发的"白屏",通过前端sdk无法监控到,所以需要通过后端模拟加载页面来查看是否出现"白屏"现象。
实现的思路参考的字节的WKWebView的性能优化方案一文,在基础上面做了优化。
#字节实现方案:
获取快照
```javascript
- (void)takeSnapshotWithConfiguration:(nullable WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(UIImage * _Nullable snapshotImage, NSError * _Nullable error))completionHandler API_AVAILABLE(ios(11.0));
```
其中snapshotConfiguration 参数可用于配置快照大小范围,默认截取当前客户端整个屏幕区域。由于可能出现导航栏成功加载而内容页却空白的特殊情况,导致非白屏像素点数增加对最终判定结果造成影响,考虑将其剔除。takeSnapshotWithConfiguration:shotConfiguration H5游戏的截屏。
```javascript
- (void)judgeLoadingStatus:(WKWebView *)webview {
if (@available(iOS 11.0, *)) {
if (webView && [webView isKindOfClass:[WKWebView class]]) {
CGFloat statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height; //状态栏高度
CGFloat navigationHeight = webView.viewController.navigationController.navigationBar.frame.size.height; //导航栏高度
WKSnapshotConfiguration *shotConfiguration = [[WKSnapshotConfiguration alloc] init];
shotConfiguration.rect = CGRectMake(0, statusBarHeight + navigationHeight, _webView.bounds.size.width, (_webView.bounds.size.height - navigationHeight - statusBarHeight)); //仅截图检测导航栏以下部分内容
[_webView takeSnapshotWithConfiguration:shotConfiguration completionHandler:^(UIImage * _Nullable snapshotImage, NSError * _Nullable error) {
//todo
}];
}
}
}
```
缩放快照
为了提升检测性能,考虑将快照缩放至1/5,减少像素点总数,从而加快遍历速度。
```javascript
- (UIImage *)scaleImage: (UIImage *)image {
CGFloat scale = 0.2;
CGSize newsize;
newsize.width = floor(image.size.width * scale);
newsize.height = floor(image.size.height * scale);
if (@available(iOS 10.0, *)) {
UIGraphicsImageRenderer * renderer = [[UIGraphicsImageRenderer alloc] initWithSize:newsize];
return [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
[image drawInRect:CGRectMake(0, 0, newsize.width, newsize.height)];
}];
}else{
return image;
}
}
```
缩小前后性能对比(实验环境:iPhone11同一页面下):
缩放前白屏检测:
![𢞍娲㒡ᢂ](image/둑𪀸翻凄)
耗时20ms
缩放后白屏检测:
注意这里有个小坑。由于缩略图的尺寸在 原图宽高*缩放系数后可能不是整数,在布置画布重绘时默认向上取整,这就造成画布比实际缩略图大(混蛋啊 摔!)。在遍历缩略图像素时,会将图外画布上的像素纳入考虑范围,导致实际白屏页 像素占比并非100% 如图所示。因此使用floor将其尺寸大小向下取整。
遍历快照
遍历快照缩略图像素点,对白色像素(R:255 G: 255 B: 255)占比大于95%的页面,认定其为白屏。(方法在ScityImg组件库中)。
##缺点
1、对屏幕进行截屏,不适用于半透明背景的网页
2、CFDataGetBytePtr有一定的概率会导致崩溃crash
#改进方案
1 采用对webview转图片的方式来判断白屏。
```javascript
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
@weakify(self, webView)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self, webView)
if (self.isHalfScreen) {
if (self.whiteScreenCheckCount > 0) {
UIImage *img = [self shotShareImageFromView:webView];
UIImage *scaleSmallImg = [self scaleImage:img];
BOOL isWhiteScreen = [self searchEveryPixel:scaleSmallImg];
self.whiteScreenCheckCount--;
if (isWhiteScreen) {
DDLogInfo(@"【WebView】白屏, %@", webView.URL.absoluteString);
[webView reload];
}
}
else {
DDLogInfo(@"【WebView】白屏,2次重试用完,报错提示,延时1秒并收起 %@", webView.URL.absoluteString);
[self.statusManager showOnlyErrorView];
self.statusManager.currentView.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self)
if (self.gobackBlock) {
self.gobackBlock();
}
});
}
}
});
```
2 解决图片缩放有概率崩溃的问题。
```javascript
- (BOOL)searchEveryPixel:(UIImage *)image {
BOOL isBlankPage = YES;
// 从背景色提取rgb分量,用于白屏检测
UIColor *color = self.webView.backgroundColor;
CGFloat bgRed = 0.0;
CGFloat bgGreen = 0.0;
CGFloat bgBlue = 0.0;
CGFloat alpha = 0.0;
[color getRed:&bgRed green:&bgGreen blue:&bgBlue alpha:&alpha];
bgRed = (NSInteger)(bgRed * 255);
bgGreen = (NSInteger)(bgGreen * 255);
bgBlue = (NSInteger)(bgBlue * 255);
// NSLog(@"__++- %@ %@ %@", @(bgRed), @(bgGreen), @(bgBlue));
// 第一步 本来是先把图片缩小 加快计算速度. 但越小结果误差可能越大
// 但是传进来的图片已经缩小过了,所以这第一步是把image搞到bitmap上
int bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast;
CGSize thumbSize = image.size;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL,thumbSize.width,thumbSize.height, 8, thumbSize.width*4, colorSpace,bitmapInfo);
CGRect drawRect = CGRectMake(0, 0, thumbSize.width, thumbSize.height);
CGContextDrawImage(context, drawRect, image.CGImage);
CGColorSpaceRelease(colorSpace);
// 第二步 取每个点的像素值
unsigned char* data = CGBitmapContextGetData (context);
if (!data) {
NSLog(@"【WebView】白屏,从Bitmap拿图片数据失败");
return isBlankPage;
}
int whiteCount = 0;
int totalCount = 0;
for (int x = 0; x < thumbSize.width; x++) {
for (int y = 0; y < thumbSize.height; y++) {
int offset = 4 * (x * y);
int red = data[offset];
int green = data[offset + 1];
int blue = data[offset + 2];
// int alpha = data[offset + 3];
totalCount ++;
// NSLog(@"++- %@, %@, %@",@(red) , @(green) , @(blue));
if ((bgRed == red) && (bgGreen == green) && (bgBlue == blue)) {
// NSLog(@"++- ++- %@, %@, %@",@(red) , @(green) , @(blue));
whiteCount++;
}
}
}
CGContextRelease(context);
float proportion = (float)whiteCount / totalCount ;
// NSLog(@"当前像素点数:%d,白色像素点数:%d , 占比: %f",totalCount , whiteCount , proportion);
if (proportion < 1) {
isBlankPage = NO;
}
return isBlankPage;
}
```
检测时机:
当WKWebView加载一个Url以后,WKWebView的url请求路径响应流程图。
![image.png](image/knowledge/e75fad0a-0038-4ab0-b735-5a59437e1240.png)
我选择了二个阶段去判断是否当前是白屏。
```javascript
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.isHalfScreen) {
if (self.whiteScreenCheckCount > 0) {
UIImage *img = [self shotShareImageFromView:webView];
UIImage *scaleSmallImg = [self scaleImage:img];
BOOL isWhiteScreen = [self searchEveryPixel:scaleSmallImg];
self.whiteScreenCheckCount--;
if (isWhiteScreen) {
NSLog(@"【WebView】白屏, %@", webView.URL.absoluteString);
[webView reload];
}
}
else {
NSLog(@"【WebView】白屏,2次重试用完,报错提示,延时1秒并收起 %@", webView.URL.absoluteString);
// [self.statusManager showOnlyErrorView];
// self.statusManager.currentView.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.gobackBlock) {
[webView goBack];
}
});
}
}
});
}
```