Подтвердить что ты не робот

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

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

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
}

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

4b9b3361

Ответ 1

Правильно, как обычно, король в пятницу. Оказывается, 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")
}

Ответ 2

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

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

Ответ 3

Решение, отправленное матом, - это то, что нужно. Думаю, я перевел бы его на objective-c code

@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"];

Ответ 4

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

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

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

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()
    }
}

Ответ 5

Я также отметил, что вам также необходимо удалить обработчик (и) сообщений во время разборки, иначе обработчик (и) останется включенным (даже если все остальное в веб-просмотре будет освобождено):

WKUserContentController *controller = 
self.webView.configuration.userContentController;

[controller removeScriptMessageHandlerForName:@"message"];