なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

Mobile Safari がクラッシュした理由をもうちょっとだけ詳しく記録したい

iOS で WebView を使っている場合、何らかの理由で WebView のプロセスが死んだ場合は webViewWebContentProcessDidTerminate が呼ばれるんですが、原因が分からず、なんとも困った感じになります。
ということで、頑張ってなんでクラッシュしたのかの大まかな理由を取得します。

理由自体は、上記メソッド名に withReason を付け足したような webContentProcessDidTerminateWithReason メソッドを定義することで受け取れます。
ただし、非公開 API であるため、通常の手順で使用することは出来ません。
ということで、まずは以下のようにヘッダーを実装します。

#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

// 該当イベントを受け取るためのインターフェース
@protocol WKNavigationDelegateExtended <WKNavigationDelegate>
@optional

- (void)webView:(WKWebView*)webView webContentProcessDidTerminateWithReason:(NSInteger)reason;

@end

// WebView のラッパー
@interface WebViewWrapper : NSObject<WKNavigationDelegate>

- (void)alloc:(WKWebView*)webView;

- (void)setNavigationDelegateExtended:(id<WKNavigationDelegate>)navigationDelegate;

@end

本体はこんな感じです。

#import "WebViewWrapper.h"

@implementation WebViewWrapper {
  __weak WKWebView* _webView;
  __weak id<WKNavigationDelegate> _navigationDelegate;
}

- (void)alloc:(WKWebView*) webView {
  _webView = webView;
  _webView.navigationDelegate = self;
}

- (void)setNavigationDelegateExtended:(id<WKNavigationDelegate>) navigationDelegate {
  _navigationDelegate = navigationDelegate;
}

- (void)_webView:(WKWebView*) webView webContentProcessDidTerminateWithReason:(NSInteger) reason {
  if (webView != _webView) {
    return;
  }

  __strong id<WKNavigationDelegateExtended> delegate = _navigationDelegate;
  if (delegate && [delegate respondsToSelector:@selector(webView:webContentProcessDidTerminateWithReason:)]) {
    [delegate webView:webView webContentProcessDidTerminateWithReason:reason];
  }
}

@end

さすがに ObjC で全部書くのはつらいので、こっからは Swift で使います。
Swift 側はこんな感じ。

import Foundation
import WebKit

class WebView : NSObject {
  private var _webView: WKWebView!
  private var _wrapper: WebViewWrapper!

  init() {
    self._webView = WKWebView(...)
    self._wrapper = WebViewWrapper()

    self._wrapper.alloc(webView: self._webView)
    self._wrapper.setNavigationDelegateExtended(self)
  }
}

extension WebView: WKNavigationDelegateExtended {
  // https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/Cocoa/NavigationState.mm#L1025-L1048
  func webView(_ webView: WKWebView, webContentProcessDidTerminateWithReason reason: NSInteger) {
    switch reason {
    case 0:
      // Memory Limit
      break

    case 1:
      // CPU Limit
      break

    case 2:
      // Request by Application
      break

    case 3:
      // Process Count Limit
      // Unresponsive
      // Request by Network
      // Request by GPU Process
      // Crash
      break

    default:
      // Unreachable
      break
    }
  }
}

これで、大まかではありますが原因がアプリ側から取得できます。
パラメータの reason には、本来 _WKProcessTerminationReason 型が渡されるのですが、実態は Enum で定義されている型なので、 NSInteger で取得しても問題ありません。
中身は、上記コードのコメントの WebKit/WebKit のソースを見ると良いでしょう。
わたしはアプリがクラッシュするから調べてーって言われて Safari の DevTools 繋いで再現した!って喜んで詳しく調べたら、結局単純に Safari がクラッシュしてるだけでした。
ということで、メモでした。
ちなみに Release ビルドに上記コードを含めるとおそらくリジェクトされると思うので、開発時だけにしましょう。

参考: