WKWebView заставляет мой контроллер представления протекать

Мой контроллер представления отображает WKWebView. Я установил обработчика сообщений, прохладная функция Web Kit, которая позволяет моему коду быть уведомленным из веб-страницы:

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    let url = // ...
    self.wv.loadRequest(NSURLRequest(URL:url))
    self.wv.configuration.userContentController.addScriptMessageHandler(
        self, name: "dummy")
}

func userContentController(userContentController: WKUserContentController,
    didReceiveScriptMessage message: WKScriptMessage) {
        // ...
}

Пока неплохо, но теперь я обнаружил, что мой контроллер представления протекает - когда он, как предполагается, освобожден, это не:

deinit {
    println("dealloc") // never called
}

Кажется, что, просто устанавливая меня, поскольку обработчик сообщений вызывает сохранить цикл и следовательно утечку!

62
задан 15 October 2014 в 16:47

4 ответа

Корректный, как обычно, Король в пятницу. Оказывается, что WKUserContentController сохраняет своего обработчика сообщений . Это делает определенное количество смысла, так как он мог едва отправить сообщение в своего обработчика сообщений, если его обработчик сообщений прекратил существование. Это параллельно способу, которым CAAnimation сохраняет своего делегата, например.

Однако это также вызывает сохранить цикл, потому что сам WKUserContentController протекает. Это не имеет значения очень самостоятельно (это только 16K), но сохранить цикл и утечка контроллера представления плохи.

Мое обходное решение должно вставить объект батута между WKUserContentController и обработчиком сообщений. Объект батута имеет только слабую ссылку на реального обработчика сообщений, таким образом, существует, не сохраняют цикл. Вот объект батута:

class LeakAvoider : NSObject, WKScriptMessageHandler {
    weak var delegate : WKScriptMessageHandler?
    init(delegate:WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }
    func userContentController(userContentController: WKUserContentController,
        didReceiveScriptMessage message: WKScriptMessage) {
            self.delegate?.userContentController(
                userContentController, didReceiveScriptMessage: message)
    }
}

Теперь, когда мы устанавливаем обработчика сообщений, мы устанавливаем объект батута вместо self:

self.wv.configuration.userContentController.addScriptMessageHandler(
    LeakAvoider(delegate:self), name: "dummy")

Это работает! Теперь deinit назван, доказав, что нет никакой утечки. Похоже, что это не должно работать, потому что мы создали наш объект LeakAvoider и никогда не держали ссылку на него; но помните, сам WKUserContentController сохраняет его, таким образом, нет никакой проблемы.

Для полноты, теперь, когда deinit назван, можно удалить обработчика сообщений там, хотя я не думаю, что это на самом деле необходимо:

deinit {
    println("dealloc")
    self.wv.stopLoading()
    self.wv.configuration.userContentController.removeScriptMessageHandlerForName("dummy")
}
126
ответ дан 31 October 2019 в 13:25

Утечка вызывается userContentController.addScriptMessageHandler(self, name: "handlerName"), который сохранит ссылку на обработчика сообщений self.

Для предотвращения утечек просто удалите обработчика сообщений через userContentController.removeScriptMessageHandlerForName("handlerName"), когда Вам больше не будет нужен он. Если Вы добавляете addScriptMessageHandler в viewDidAppear, это - хорошая идея удалить его в viewDidDisappear.

22
ответ дан 31 October 2019 в 13:25

Решение, отправленное матовым, что необходимо. Мысль я перевел бы его в объективный-c код

@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>

@property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;

@end

@implementation WeakScriptMessageDelegate

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate
{
    self = [super init];
    if (self) {
        _scriptDelegate = scriptDelegate;
    }
    return self;
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}

@end

Затем, использует его как это:

WKUserContentController *userContentController = [[WKUserContentController alloc] init];    
[userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"name"];
18
ответ дан 31 October 2019 в 13:25

Основная проблема: WKUserContentController содержит сильную ссылку ко всем WKScriptMessageHandlers, которые были добавлены к нему. Необходимо удалить их вручную.

, Так как это - все еще проблема с Swift 4.2 и iOS 11, я хочу предложить решение, которое использует обработчик, который является отдельным от контроллера представления, который содержит UIWebView. Таким образом, контроллер представления обычно может deinit и говорить обработчику мыться также.

Вот мое решение:

UIViewController:

import UIKit
import WebKit

class MyViewController: JavascriptMessageHandlerDelegate {

    private let javascriptMessageHandler = JavascriptMessageHandler()

    private lazy var webView: WKWebView = WKWebView(frame: .zero, configuration: self.javascriptEventHandler.webViewConfiguration)

    override func viewDidLoad() {
        super.viewDidLoad()

        self.javascriptMessageHandler.delegate = self

        // TODO: Add web view to the own view properly

        self.webView.load(URLRequest(url: myUrl))
    }

    deinit {
        self.javascriptEventHandler.cleanUp()
    }
}

// MARK: - JavascriptMessageHandlerDelegate
extension MyViewController {
    func handleHelloWorldEvent() {

    }
}

Обработчик:

import Foundation
import WebKit

protocol JavascriptMessageHandlerDelegate: class {
    func handleHelloWorld()
}

enum JavascriptEvent: String, CaseIterable {
    case helloWorld
}

class JavascriptMessageHandler: NSObject, WKScriptMessageHandler {

    weak var delegate: JavascriptMessageHandlerDelegate?

    private let contentController = WKUserContentController()

    var webViewConfiguration: WKWebViewConfiguration {
        for eventName in JavascriptEvent.allCases {
            self.contentController.add(self, name: eventName.rawValue)
        }

        let config = WKWebViewConfiguration()
        config.userContentController = self.contentController

        return config
    }

    /// Remove all message handlers manually because the WKUserContentController keeps a strong reference on them
    func cleanUp() {
        for eventName in JavascriptEvent.allCases {
            self.contentController.removeScriptMessageHandler(forName: eventName.rawValue)
        }
    }

    deinit {
        print("Deinitialized")
    }
}

// MARK: - WKScriptMessageHandler
extension JavascriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        // TODO: Handle messages here and call delegate properly
        self.delegate?.handleHelloWorld()
    }
}
1
ответ дан 31 October 2019 в 13:25

Другие вопросы по тегам:

Похожие вопросы: