WKWebView

UIWebView가 deprecated된 이후 웹 페이지를 렌더링 하는데 선택지는 WKWebView만이 남았습니다.
WKWebView는 UIWebView보다 높은 성능을 자랑하지만, 잘못된 설계? 로 인해 쉽게 메모리 누수가 발생할 수 있습니다. 물론 잘 사용한다면 메모리 누수 없이 사용할 수 있습니다.
WKWebView를 사용하면서 쉽게 실수할 수 있는 부분을 소개하고 어떻게 해결할 수 있을지 소개합니다.

WKWebView Memory leak

구글에 WKWebView Memory leak로 검색을 하면 많은 결과물이 쏟아져 나옵니다. 그만큼 WKWebView에서 메모리 누수는 대부분의 앱들이 겪고 있으며 무시하고 지나갔다가는 서비스가 성장함에 따라 앱이 무거워지고 사용자가 증가하면서 이슈로 때려 맞을 가능성이 높아집니다.

WKWebView 에서 메모리 누수가 발생하는 주요 원인 2가지와 해결책

1. [Problem] message handler를 잘못 설계하고 사용한다.

앱 <-> 웹의 소통 방식으로 MessageHandler 방식을 많이 사용합니다.

let config = WKWebViewConfiguration()
let content = WKUserContentController()

content.add(self, name: "callback")
config.userContentController = content

page = WKWebView(frame: self.WKBaseView.bounds, configuration: config)

간략하게 설명하면 “callback” 이라는 메시지를 등록하고 WKWebView에 configuration으로 파라미터를 넘겨서 생성하면, 자바스크립트로 callback이라는 이름의 함수를 호출하면

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

}

위 WKMessageHandler delegate로 이벤트를 받아 특정 로직을 수행할 수 있도록 합니다. WKWebView 를 사용하는 방법을 다루는 많은 블로그에서 위와 같이 소개가 되어있고, 개발할 때 복붙하고 메시지 이름만 수정해서 사용하기 쉽습니다.

이렇게 사용하면 100% 메모리 누수가 발생합니다.

왜?

Message Handler를 등록하는 content.add(self, name: "callback") 구문에 치명적인 문제가 있습니다.
add함수로 넘어가는 self는 Strong 입니다.

즉, self(WebView) 는 메시지 핸들러를 소유하고, 메시지핸들러는 self를 소유하게되므로 순환참조가 발생하고 이는 메모리 누수로 이어집니다.
개인적으로 add 함수는 애플에서 weak로 넘겨받도록 설계가 되었어야 하지 않나? 라는 생각이 듭니다….

무튼, 결국 웹뷰를 사용하는 개발자는 이 순환참조를 막아야 하며 트램펄린 패턴으로 상호참조 문제를 해결할 수 있습니다.
아래와 같은 트램펄린 클래스를 추가합니다.

class LeakAvoider: NSObject, WKScriptMessageHandler {
    weak var delegate: WKScriptMessageHandler?
    init(delegate: WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        self.delegate?.userContentController(userContentController, didReceive: message)
    }
}

이 객체를 이용해서 아래와 같이 Message Handler를 등록합니다.

configuration.userContentController.add(LeakAvoider(delegate: self), name: "callback")

이렇게 추가되면 self는 LeakAvoider에 의해 reference count가 증가되지 않는 상태로 등록될 수 있습니다.

전체 코드는 아래와 같습니다.

import UIKit
import WebKit

class LeakAvoider: NSObject, WKScriptMessageHandler {
    weak var delegate: WKScriptMessageHandler?
    init(delegate: WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        self.delegate?.userContentController(userContentController, didReceive: message)
    }
}

class ViewController: UIViewController, WKScriptMessageHandler {
    var webview: WKWebView!

    override func viewDidLoad() {
        let configuration = WKWebViewConfiguration()
        configuration.userContentController.add(LeakAvoider(delegate: self), name: "callback")

        webview = WKWebView(frame: .zero, configuration: configuration)
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        // 메시지를 처리합니다
    }
}

2. RxWebkit을 이용하고 WKNavigationDelegate를 RxWebkit을 이용해 호출한다.

프로젝트를 Rx로 구현하고 있는 경우 WKWebView를 Rx로 구현하기 위해 RxWebKit 라이브러리를 사용할 수 있습니다.
이 때 WKNavigationDelegate 메소드를 RxWebkit으로 구현하는 경우 Strong으로 잡힙니다…

아마 라이브러리 이슈로 보이며 WKNavigationDelegate의 경우에는 RxWebkit 메소드를 이용하지 않고 직접 구현함으로써 회피할 수 있습니다.

0%