我的视图控制器显示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
}
似乎只是将自己安装为消息处理程序会导致保留周期,从而导致泄漏!
答案 0 :(得分:116)
像往常一样正确,星期五。事实证明,WKUserContentController 保留了其消息处理程序。这有一定意义,因为如果消息处理程序不再存在,它很难向消息处理程序发送消息。例如,它与CAAnimation保留其委托的方式并行。
但是,它也会导致保留周期,因为WKUserContentController本身正在泄漏。这本身并不重要(它只有16K),但保留周期和视图控制器的泄漏都很糟糕。
我的解决方法是在WKUserContentController和消息处理程序之间插入一个trampoline对象。 trampoline对象只有对真实消息处理程序的弱引用,因此没有保留周期。这是蹦床对象:
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)
}
}
现在,当我们安装消息处理程序时,我们安装了trampoline对象而不是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")
}
答案 1 :(得分:21)
泄漏是由userContentController.addScriptMessageHandler(self, name: "handlerName")
引起的,它将保留对消息处理程序self
的引用。
为了防止泄漏,只需在不再需要时通过userContentController.removeScriptMessageHandlerForName("handlerName")
删除邮件处理程序。如果您在viewDidAppear
添加addScriptMessageHandler,最好在viewDidDisappear
中删除它。
答案 2 :(得分:17)
matt发布的解决方案正是我们所需要的。以为我会把它翻译成objective-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"];
答案 3 :(得分:1)
我还注意到,您还需要在拆卸期间删除消息处理程序,否则处理程序将继续存在(即使有关Webview的所有其他内容都已释放):
WKUserContentController *controller =
self.webView.configuration.userContentController;
[controller removeScriptMessageHandlerForName:@"message"];
答案 4 :(得分:1)
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)
}
}
不要忘记在此处粘贴解决方案代码
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>
答案 5 :(得分:0)
基本问题: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()
}
}
答案 6 :(得分:0)
@matt 完美地描述了视图控制器泄漏的原因,我建议使用指向 self 的弱指针并将其用作函数参数。
required init?(coder: NSCoder) {
super.init(coder: coder)
self.weakSelf = self
}
...
webView.configuration.userContentController.add(weakSelf, name: "dummy")
...
private weak var weakSelf: WKScriptMessageHandler!
这解决了释放视图控制器的问题,但是如果你看看Instruments->Leaks ;), webView 对象退出并保留计数=1。我做了一些研究并意识到传递给函数的引用类型(强或弱)无关紧要,一件事很重要 - 你必须调用:
webView.configuration.userContentController.removeScriptMessageHandler(forName: "dummy")
我建议在 viewWillDisappear() 方法中这样做。