一种检测WKWebView白屏检测的优化方案

##什么是“白屏时间” 

白屏时间是指用户输入网站地址到浏览器开始显示内容的时间。当用户打开一个链接或者在浏览器输入一个地址开始访问的时候,就开始等待页面的展示,页面渲染的时间越短,用户等待的时间就越短,用户的感知越好,减少用户的跳出,这样可以极大的提升用户的体验。需要注意的是白屏并不是特指屏幕为白色,"白屏"也有可能是黑色的,重点是用户等待的时间过长,或者页面异常导致没有有效的页面,此类情况,我们都称之为"白屏"。

 页面的白屏情况有以下几种:

 (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];

                    }

                });

            }

        }

    });

}

```

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容