iOS WKWebview白画面検出実装例


前言
    iOS 8がwkwebviewを発売して以来、ウェブページのロード速度とメモリの漏洩問題が大幅に改善され、徐々に重いUICWebviewに取って代わるようになりました。高機能で高刷新のWKWebviewはハイブリッド開発において活躍していますが、ページを読み込む過程で異常な白画面が発生する現象が後を絶たず、既存のapiプロトコル処理ではこのような異常なcaseが捉えられず、ユーザーが無駄な待ち時間を経験するのは非常に悪いです。
    業務シーンの需要に対して、白スクリーンのロードテストを実現します。バイト鼓動チームで提案したwebview最適化技術案を考慮します。適切なロードタイミングで現在のwebviewの可視領域をスクリーンショットし、このスナップショットに対してピクセルドットを巡回し、非ホワイトスクリーン色のピクセルポイントが一定の閾値を超えたら、非ホワイトスクリーンとして認識し、逆に要求を再ロードする。
スナップショットをとる
    iosの公式はwebviewのスナップショットインターフェースを簡単に入手し、非同期で現在の視認エリアのスクリーンショットを取得します。

- (void)takeSnapshotWithConfiguration:(nullable WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(UIImage * _Nullable snapshotImage, NSError * _Nullable error))completionHandler API_AVAILABLE(ios(11.0));
snapshotConfigrationパラメータは、スナップショットのサイズ範囲を設定するために使用でき、デフォルトでは、現在のクライアント全体の画面領域を切り取ります。ナビゲーションバーの読み込みに成功してコンテンツページが空白になる場合がありますので、白画面ではないピクセル数の増加が最終判定結果に影響を与え、削除することを検討しています。

- (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;
  }
}
前と後の性能比較を縮小する(実験環境:iPhone 11同一ページ下):
ズーム前の白画面の検出:


20 msかかります
ズーム後の白画面の検出:


13 msの時間がかかります
     ここに小さな穴があります。サムネイルのサイズは原図の幅の高さ*ズーム係数の後は整数ではないかもしれませんので、キャンバスを配置して絵を描き直す時はデフォルトで上に丸めます。これはキャンバスが実際のサムネイルより大きいです。サムネイルピクセルを巡回すると、図外のカンバス上のピクセルを考慮範囲に入れます。実際のホワイトスクリーンページのピクセル比率は100%ではありません。そのためflorを使ってサイズを下に整えます。
スナップショットを経る
    スナップショットのサムネイルのピクセルポイントを巡回して、白いピクセル(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 = 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) {
        if (snapshotImage) {
          CGImageRef imageRef = snapshotImage.CGImage;
          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);
//      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;
  }
}

//    
- (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;
  }
}
この関数法を適用して適切なviewライフサイクル内でフィードバックするだけで、ページの状態がホワイトスクリーンであるかどうかを検出できます。性能損失は無視できます。
宣言
作者:BBTime
リンク:https://juejin.im/post/6885298718174609415
以上はiOS WKWebviewホワイトスクリーンの検出が実現された例の詳細です。iOS WKWebviewホワイトスクリーンの検出に関する資料は他の関連記事に注目してください。