一种检测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];

                    }

                });

            }

        }

    });

}

```

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容