Richtig wie immer, King Friday. Es stellt sich heraus, dass der WKUserContentController seinen Nachrichtenhandler beibehält . Dies ist in gewissem Maße sinnvoll, da es kaum eine Nachricht an seinen Nachrichtenhandler senden könnte, wenn sein Nachrichtenhandler nicht mehr existiert. Dies ist parallel dazu, wie eine CAAnimation beispielsweise ihren Delegaten behält.
Es verursacht jedoch auch einen Aufbewahrungszyklus, da der WKUserContentController selbst undicht ist. Das allein macht nicht viel aus (es sind nur 16 KB), aber der Haltezyklus und das Leck des View Controllers sind schlecht.
Meine Problemumgehung besteht darin, ein Trampolinobjekt zwischen dem WKUserContentController und dem Nachrichtenhandler einzufügen. Das Trampolinobjekt hat nur einen schwachen Bezug zum realen Nachrichtenhandler, daher gibt es keinen Aufbewahrungszyklus. Hier ist das Trampolin-Objekt:
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)
}
}
Wenn wir jetzt den Nachrichtenhandler installieren, installieren wir das Trampolinobjekt anstelle von self
:
self.wv.configuration.userContentController.addScriptMessageHandler(
LeakAvoider(delegate:self), name: "dummy")
Es klappt! Jetzt deinit
wird aufgerufen, um zu beweisen, dass es kein Leck gibt. Es sieht so aus, als ob dies nicht funktionieren sollte, da wir unser LeakAvoider-Objekt erstellt haben und nie einen Verweis darauf hatten. Denken Sie jedoch daran, dass der WKUserContentController es selbst beibehält, sodass es kein Problem gibt.
Der Vollständigkeit deinit
halber können Sie den Nachrichtenhandler dort deinstallieren, obwohl ich dies nicht für notwendig halte:
deinit {
println("dealloc")
self.wv.stopLoading()
self.wv.configuration.userContentController.removeScriptMessageHandlerForName("dummy")
}
WKUserContentController
Nachrichtenhandler (self), der das Leck verursacht, beibehalten wird, sollteweak self
ARC nicht dazu führen, dass die Referenzanzahl von my nicht erhöht wirdself
. Wenn also der andere alleinige Referent des Selbst nicht mehr darauf zeigt, sollte er freigegeben werden?Das Leck wird verursacht,
userContentController.addScriptMessageHandler(self, name: "handlerName")
wodurch ein Verweis auf den Nachrichtenhandler erhalten bleibtself
.Um Undichtigkeiten zu vermeiden, entfernen
userContentController.removeScriptMessageHandlerForName("handlerName")
Sie einfach den Nachrichtenhandler über, wenn Sie ihn nicht mehr benötigen. Wenn Sie den addScriptMessageHandler unter hinzufügenviewDidAppear
, ist es eine gute Idee, ihn zu entfernenviewDidDisappear
.quelle
deinit
(Objective-Cdealloc
), aber es wird nie aufgerufen, weil (warten Sie darauf) wir undicht sind! Das ist das Problem, das meine Trampolinlösung löst. Übrigens, dasselbe Problem und dieselbe Lösung setzen sich auch in iOS 9 fort.Die von matt veröffentlichte Lösung ist genau das, was benötigt wird. Ich dachte, ich würde es in Objective-C-Code übersetzen
@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
Dann nutzen Sie es so:
WKUserContentController *userContentController = [[WKUserContentController alloc] init]; [userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"name"];
quelle
Ich habe auch festgestellt, dass Sie auch die Nachrichtenhandler während des Herunterfahrens entfernen müssen, da die Handler sonst weiterhin aktiv sind (auch wenn alles andere an der Webansicht freigegeben ist):
WKUserContentController *controller = self.webView.configuration.userContentController; [controller removeScriptMessageHandlerForName:@"message"];
quelle
Grundproblem: Der WKUserContentController enthält einen starken Verweis auf alle hinzugefügten WKScriptMessageHandler. Sie müssen sie manuell entfernen.
Da dies bei Swift 4.2 und iOS 11 immer noch ein Problem darstellt, möchte ich eine Lösung vorschlagen, bei der ein Handler verwendet wird, der vom Ansichtscontroller getrennt ist, in dem sich UIWebView befindet. Auf diese Weise kann der View Controller normal deinitieren und den Handler anweisen, ebenfalls zu bereinigen.
Hier ist meine Lösung:
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() { } }
Handler:
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() } }
quelle
Einzelheiten
Lösung
protocol ScriptMessageHandlerDelegate: class { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) } class ScriptMessageHandler: NSObject, WKScriptMessageHandler { deinit { print("____ DEINITED: \(self)") } private var configuration: WKWebViewConfiguration! private weak var delegate: ScriptMessageHandlerDelegate? private var scriptNamesSet = Set<String>() init(configuration: WKWebViewConfiguration, delegate: ScriptMessageHandlerDelegate) { self.configuration = configuration self.delegate = delegate super.init() } func deinitHandler() { scriptNamesSet.forEach { configuration.userContentController.removeScriptMessageHandler(forName: $0) } configuration = nil } func registerScriptHandling(scriptNames: [String]) { for scriptName in scriptNames { if scriptNamesSet.contains(scriptName) { continue } configuration.userContentController.add(self, name: scriptName) scriptNamesSet.insert(scriptName) } } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { delegate?.userContentController(userContentController, didReceive: message) } }
Vollständige Probe
import UIKit import WebKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) button.setTitle("WebView", for: .normal) view.addSubview(button) button.center = view.center button.addTarget(self, action: #selector(touchedUpInsed(button:)), for: .touchUpInside) button.setTitleColor(.blue, for: .normal) } @objc func touchedUpInsed(button: UIButton) { let viewController = WebViewController() present(viewController, animated: true, completion: nil) } } class WebViewController: UIViewController { private weak var webView: WKWebView! private var scriptMessageHandler: ScriptMessageHandler! private let url = URL(string: "http://google.com")! deinit { scriptMessageHandler.deinitHandler() print("____ DEINITED: \(self)") } override func viewDidLoad() { super.viewDidLoad() let configuration = WKWebViewConfiguration() scriptMessageHandler = ScriptMessageHandler(configuration: configuration, delegate: self) let scriptName = "GetUrlAtDocumentStart" scriptMessageHandler.registerScriptHandling(scriptNames: [scriptName]) let jsScript = "webkit.messageHandlers.\(scriptName).postMessage(document.URL)" let script = WKUserScript(source: jsScript, injectionTime: .atDocumentStart, forMainFrameOnly: true) configuration.userContentController.addUserScript(script) let webView = WKWebView(frame: .zero, configuration: configuration) self.view.addSubview(webView) self.webView = webView webView.translatesAutoresizingMaskIntoConstraints = false webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true webView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true view.bottomAnchor.constraint(equalTo: webView.bottomAnchor).isActive = true view.rightAnchor.constraint(equalTo: webView.rightAnchor).isActive = true webView.load(URLRequest(url: url)) } } extension WebViewController: ScriptMessageHandlerDelegate { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { print("received \"\(message.body)\" from \"\(message.name)\" script") } }
Info.plist
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>
quelle