WKWebView学习笔记II - 关于加载WKWebView白屏问题的记录

处理办法,转载自iOS__峰的博客:ios

自ios8推出wkwebview以来,极大改善了网页加载速度及内存泄漏问题,逐渐全面取代笨重的UIWebview。尽管高性能、高刷新的WKWebview在混合开发中大放异彩表现优异,但加载网页过程当中出现异常白屏的现象却仍然家常便饭,且现有的api协议处理捕捉不到这种异常case,形成用户无用等待体验不好。web

针对业务场景需求,实现加载白屏检测。考虑采用字节跳动团队提出的webview优化技术方案。在合适的加载时机对当前webview可视区域截图,并对此快照进行像素点遍历,若是非白屏颜色的像素点超过必定的阈值,认定其为非白屏,反之从新加载请求。api

IOS官方提供了简易的获取webview快照接口,经过异步回调拿到当前可视区域的屏幕截图。以下:服务器

- (void)takeSnapshotWithConfiguration:(nullable WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(UIImage * _Nullable snapshotImage, NSError * _Nullable error))completionHandler API_AVAILABLE(ios(11.0));markdown

函数描述 : 获取WKWebView可见视口的快照。若是WKSnapshotConfiguration为nil,则该方法将快照WKWebView并建立一个图像,该图像是WKWebView边界的宽度,并缩放到设备规模。completionHandler被用来传递视口内容的图像或错误。异步

参数 :ide

snapshotConfiguration : 指定如何配置快照的对象函数

completionHandler : 快照就绪时要调用的块。oop

- (void)takeSnapshotWithConfiguration:(nullable WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(UIImage * _Nullable snapshotImage, NSError * _Nullable error))completionHandler API_AVAILABLE(ios(11.0));
复制代码

其中snapshotConfiguration 参数可用于配置快照大小范围,默认截取当前客户端整个屏幕区域。因为可能出现导航栏成功加载而内容页却空白的特殊状况,致使非白屏像素点数增长对最终断定结果形成影响,考虑将其剔除。以下:布局

- (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,减小像素点总数,从而加快遍历速度。以下 :

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

image

耗时20ms。

缩放后白屏检测:

image

image

耗时13ms。

注意这里有个小坑。因为缩略图的尺寸在 原图宽高*缩放系数后可能不是整数,在布置画布重绘时默认向上取整,这就形成画布比实际缩略图大(混蛋啊 摔!)。在遍历缩略图像素时,会将图外画布上的像素归入考虑范围,致使实际白屏页 像素占比并不是100% 如图所示。所以使用floor将其尺寸大小向下取整。

遍历快照缩略图像素点,对白色像素(R:255 G: 255 B: 255)占比大于95%的页面,认定其为白屏。以下

- (BOOL)searchEveryPixel:(UIImage *)image {
    CGImageRef cgImage = [image CGImage];
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    size_t bytesPerRow = CGImageGetBytesPerRow(cgImage); //每一个像素点包含r g b a 四个字节
    size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage);
 
    CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
    CFDataRef data = CGDataProviderCopyData(dataProvider);
    UInt8 * buffer = (UInt8*)CFDataGetBytePtr(data);
 
    int whiteCount = 0;
    int totalCount = 0;
 
    for (int j = 0; j < height; j ++ ) {
        for (int i = 0; i < width; i ++) {
            UInt8 * pt = buffer + j * bytesPerRow + i * (bitsPerPixel / 8);
            UInt8 red   = * pt;
            UInt8 green = *(pt + 1);
            UInt8 blue  = *(pt + 2);
//            UInt8 alpha = *(pt + 3);
 
            totalCount ++;
            if (red == 255 && green == 255 && blue == 255) {
                whiteCount ++;
            }
        }
    }
    float proportion = (float)whiteCount / totalCount ;
    NSLog(@"当前像素点数:%d,白色像素点数:%d , 占比: %f",totalCount , whiteCount , proportion );
    if (proportion > 0.95) {
        return YES;
    }else{
        return NO;
    }
} 
复制代码

总结:

typedef NS_ENUM(NSUInteger,webviewLoadingStatus) {
 
    WebViewNormalStatus = 0, //正常
 
    WebViewErrorStatus, //白屏
 
    WebViewPendStatus, //待决
};

/// 判断是否白屏
- (void)judgeLoadingStatus:(WKWebView *)webview  withBlock:(void (^)(webviewLoadingStatus status))completionBlock{
    webviewLoadingStatus __block status = WebViewPendStatus;
    if (@available(iOS 11.0, *)) {
        if (webview && [webview isKindOfClass:[WKWebView class]]) {
 
            CGFloat statusBarHeight =  [[UIApplication sharedApplication] statusBarFrame].size.height; //状态栏高度
            CGFloat navigationHeight = self.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) {
                if (snapshotImage) {
                    UIImage * scaleImage = [self scaleImage:snapshotImage];
                    BOOL isWhiteScreen = [self searchEveryPixel:scaleImage];
                    if (isWhiteScreen) {
                       status = WebViewErrorStatus;
                    }else{
                       status = WebViewNormalStatus;
                    }
                }
                if (completionBlock) {
                    completionBlock(status);
                }
            }];
        }
    }
}
 
/// 遍历像素点 白色像素占比大于95%认定为白屏
- (BOOL)searchEveryPixel:(UIImage *)image {
    CGImageRef cgImage = [image CGImage];
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    size_t bytesPerRow = CGImageGetBytesPerRow(cgImage); //每一个像素点包含r g b a 四个字节
    size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage);
 
    CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
    CFDataRef data = CGDataProviderCopyData(dataProvider);
    UInt8 * buffer = (UInt8*)CFDataGetBytePtr(data);
 
    int whiteCount = 0;
    int totalCount = 0;
 
    for (int j = 0; j < height; j ++ ) {
        for (int i = 0; i < width; i ++) {
            UInt8 * pt = buffer + j * bytesPerRow + i * (bitsPerPixel / 8);
            UInt8 red   = * pt;
            UInt8 green = *(pt + 1);
            UInt8 blue  = *(pt + 2);
 
            totalCount ++;
            if (red == 255 && green == 255 && blue == 255) {
                whiteCount ++;
            }
        }
    }
    float proportion = (float)whiteCount / totalCount ;
    NSLog(@"当前像素点数:%d,白色像素点数:%d , 占比: %f",totalCount , whiteCount , proportion );
    if (proportion > 0.95) {
        return YES;
    }else{
        return NO;
    }
}
 
///缩放图片
- (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;
    }
}

复制代码

在页面加载完成的代理函数中进行判断,决定是否从新进行加载,以下:

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [self judgeLoadingStatus:webView withBlock:^(webviewLoadingStatus status) {
        if(status == WebViewNormalStatus){
            //页面状态正常
            [self stopIndicatorWithImmediate:NO afterDelay:1.5f indicatorString:@"页面加载完成" complete:nil];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self.webView evaluateJavaScript:@"signjsResult()" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
                }];
            });
        }else{
            //可能发生了白屏,刷新页面
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSURLRequest *request = [[NSURLRequest alloc]initWithURL:webView.URL];
                [self.webView loadRequest:request];
            });
        }
    }];
}
复制代码

通过测试,发现页面加载白屏时函数确实能够检测到。通过排查,发现形成偶性白屏时,控制台输出内容以下 : urface creation failed for size。是因为布局问题产生了页面白屏,白屏前设置约束的代码以下 :

- (void)viewDidLoad {
    [super viewDidLoad];
    //设置控制器标题
    self.title = self.model.menuName;
    //设置服务器URL字符串
    NSString *str;
    str =[NSString stringWithFormat:@"%@%@?access_token=%@",web_base_url,self.model.request,access_token_macro];
    NSLog(@"======:%@",str);
    str = [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    //设置web视图属性
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    WKPreferences *preference = [[WKPreferences alloc]init];
    configuration.preferences = preference;
    configuration.selectionGranularity = YES; //容许与网页交互
    //设置web视图
    self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
    self.webView.navigationDelegate = self;
    self.webView.UIDelegate = self;
    [self.webView.scrollView setShowsVerticalScrollIndicator:YES];
    [self.webView.scrollView setShowsHorizontalScrollIndicator:YES];
       [self.view addSubview:self.webView];
    [[self.webView configuration].userContentController addScriptMessageHandler:self name:@"signjs"];
    /* 加载服务器url的方法*/
    NSString *url = str;
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20];
    [self.webView loadRequest:request];
    
}

///设置视图约束
- (void)updateViewConstraints {
    [self.webView mas_makeConstraints:^(MASConstraintMaker *make) {
        if (@available(iOS 11.0, *)) {
            make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
        } else {
            make.top.equalTo(self.view.mas_topMargin);
        }
        make.left.bottom.right.equalTo(self.view);
    }];
    [super updateViewConstraints];
}
复制代码

改成再将WKWebView视图添加到控制器视图后,直接设置约束,偶发性白屏问题消失,因为布局问题引起的白屏,即便从新加载页面也不会解决的,会陷入死循环当中。以下 :

- (void)viewDidLoad {
    [super viewDidLoad];
    //设置控制器标题
    self.title = self.model.menuName;
    //设置服务器URL字符串
    NSString *str;
    str =[NSString stringWithFormat:@"%@%@?access_token=%@",web_base_url,self.model.request,access_token_macro];
    NSLog(@"======:%@",str);
    str = [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    //设置web视图属性
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    WKPreferences *preference = [[WKPreferences alloc]init];
    configuration.preferences = preference;
    configuration.selectionGranularity = YES; //容许与网页交互
    //设置web视图
    self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
    self.webView.navigationDelegate = self;
    self.webView.UIDelegate = self;
    [self.webView.scrollView setShowsVerticalScrollIndicator:YES];
    [self.webView.scrollView setShowsHorizontalScrollIndicator:YES];
    [[self.webView configuration].userContentController addScriptMessageHandler:self name:@"signjs"];
    [self.view addSubview:self.webView];
    [self.webView mas_makeConstraints:^(MASConstraintMaker *make) {
        if (@available(iOS 11.0, *)) {
            make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
        } else {
            make.top.equalTo(self.view.mas_topMargin);
        }
        make.left.bottom.right.equalTo(self.view);
    }];
    /* 加载服务器url的方法*/
    NSString *url = str;
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20];
    [self.webView loadRequest:request];
    
}
复制代码
相关文章
相关标签/搜索