處理辦法,轉載自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同一頁面下):
縮放前白屏檢測:
耗時20ms。
縮放後白屏檢測:
耗時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];
}
複製代碼