なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ

書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

2023/05/10

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

カテゴリー:

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

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

WebViewWrapper.h
#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

本体はこんな感じです。

WebViewWrapper.mm
#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 側はこんな感じ。

WebView.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 ビルドに上記コードを含めるとおそらくリジェクトされると思うので、開発時だけにしましょう。

参考:

Copyright (c) 2015 - 2023 Natsuneko <me@natsuneko.cat>