Skip to content

Commit ac87971

Browse files
barijaonaTAKeanice
andcommitted
Fix memory leaks through BrowserTab
Through the debugger, one could see that after they were closed, tabs were remaining in memory: the `deinit` routine was never called. Leak caused by passing delegate for link hover displaying as script message handler. This led to a retain cycle: delegate retains WebView retains Configuration retains UserContentController retains delegate (as script listener) To break it, adopt the same strategy as with the other script handlers: - Custom listener object that only weakly retains the HoverUiDelegate - The delegate is only weakly retained by the CustomWkWebView Thus, the UserContentController transitively only holds a weak reference to the delegate. Also, put the link hover ui into BrowserTab+Interface.swift because it purely deals with the UI. Cherry-pick and squash of commit 8ee5d9c and commit a884081 Issue #1667 Co-authored-by: Tassilo Karge <[email protected]>
1 parent db6345c commit ac87971

File tree

7 files changed

+98
-49
lines changed

7 files changed

+98
-49
lines changed

Vienna.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
2F88B2A62545BB450067EEA6 /* ArticleConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2F88B2A52545BB450067EEA6 /* ArticleConverter.m */; };
7272
2F88B2AD2545BCA90067EEA6 /* WebViewArticleConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F88B2AC2545BCA90067EEA6 /* WebViewArticleConverter.swift */; };
7373
2FA946C52517D8D8006134C5 /* CustomWKUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA946C42517D8D8006134C5 /* CustomWKUIDelegate.swift */; };
74+
2FB5A9E929E4BCD800409BA5 /* CustomWKHoverUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB5A9E829E4BCD800409BA5 /* CustomWKHoverUIDelegate.swift */; };
7475
2FC4A190257CF775005FF227 /* WebKitArticleConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC4A18F257CF775005FF227 /* WebKitArticleConverter.swift */; };
7576
2FC4A1BF25852C0E005FF227 /* ArticleConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC4A1BE25852C0E005FF227 /* ArticleConverter.swift */; };
7677
2FC91DDD21850ADB00D7F223 /* Browser+WebUIDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FC91DDB21850ADB00D7F223 /* Browser+WebUIDelegate.m */; };
@@ -308,6 +309,7 @@
308309
2F88B2AC2545BCA90067EEA6 /* WebViewArticleConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewArticleConverter.swift; sourceTree = "<group>"; };
309310
2F88B2C22545DAB40067EEA6 /* ArticleViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ArticleViewDelegate.h; sourceTree = "<group>"; };
310311
2FA946C42517D8D8006134C5 /* CustomWKUIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomWKUIDelegate.swift; sourceTree = "<group>"; };
312+
2FB5A9E829E4BCD800409BA5 /* CustomWKHoverUIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomWKHoverUIDelegate.swift; sourceTree = "<group>"; };
311313
2FC4A17A25797A3D005FF227 /* WKPreferences+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WKPreferences+Private.h"; sourceTree = "<group>"; };
312314
2FC4A18F257CF775005FF227 /* WebKitArticleConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitArticleConverter.swift; sourceTree = "<group>"; };
313315
2FC4A1BE25852C0E005FF227 /* ArticleConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleConverter.swift; sourceTree = "<group>"; };
@@ -1097,6 +1099,7 @@
10971099
2FC4A17A25797A3D005FF227 /* WKPreferences+Private.h */,
10981100
2F82DCA925CED70500F00EE8 /* WKWebView+Private.h */,
10991101
F6879288270117AF009B7BB5 /* WKWebViewConfiguration+Private.h */,
1102+
2FB5A9E829E4BCD800409BA5 /* CustomWKHoverUIDelegate.swift */,
11001103
);
11011104
name = CustomWebKit;
11021105
sourceTree = "<group>";
@@ -2044,6 +2047,7 @@
20442047
2F125EE024D066ED00868E11 /* AsyncHelper.swift in Sources */,
20452048
AA71168F09BEB17300E3EA8A /* AddressBarCell.m in Sources */,
20462049
0379E2381A2082E600D58BFE /* AppearancePreferencesViewController.m in Sources */,
2050+
2FB5A9E929E4BCD800409BA5 /* CustomWKHoverUIDelegate.swift in Sources */,
20472051
F63833D81EF842EA00E07213 /* MainWindowController.swift in Sources */,
20482052
F41CA694218F56140072F734 /* NSWorkspace+OpenWithMenu.m in Sources */,
20492053
2FF61D3024F2ED1500A7E2B5 /* BrowserTab+Interface.swift in Sources */,

Vienna/Sources/Main window/BrowserTab+Interface.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,23 @@ extension BrowserTab: NSTextFieldDelegate {
166166
// TODO: things like address suggestion etc
167167
// TODO: restore url string when user presses escape in textfield, make webview first responder
168168
}
169+
170+
// MARK: hover link ui
171+
172+
extension BrowserTab: CustomWKHoverUIDelegate {
173+
174+
func viewDidLoadHoverLinkUI() {
175+
registerNavigationStartHandler { [weak self] in
176+
//TODO is it really necessary to do this on every navigation start instead of only once after webview initialization?
177+
self?.webView.hoverUiDelegate = self
178+
self?.statusBar?.label = ""
179+
}
180+
}
181+
182+
func hovered(link: String?) {
183+
guard let statusBar = statusBar else {
184+
return
185+
}
186+
statusBar.label = link
187+
}
188+
}

Vienna/Sources/Main window/BrowserTab.swift

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class BrowserTab: NSViewController {
7676
private var urlObservation: NSKeyValueObservation?
7777
private var statusBarObservation: NSKeyValueObservation?
7878

79-
private var statusBar: OverlayStatusBar?
79+
private(set) var statusBar: OverlayStatusBar?
8080

8181
// MARK: object lifecycle
8282

@@ -171,6 +171,8 @@ class BrowserTab: NSViewController {
171171

172172
self.viewDidLoadRss()
173173

174+
self.viewDidLoadHoverLinkUI()
175+
174176
updateWebViewInsets()
175177

176178
// set up address bar handling
@@ -301,13 +303,8 @@ extension BrowserTab: Tab {
301303

302304
func closeTab() {
303305
stopLoadingTab()
304-
// free webView by force stopping JavaScript and resetting delegates
305-
self.webView.evaluateJavaScript("window.location.replace('about:blank');") { _, _ in
306-
DispatchQueue.main.async {
307-
self.webView.navigationDelegate = nil
308-
self.webView.uiDelegate = nil
309-
}
310-
}
306+
// free webView by force stopping JavaScript
307+
self.webView.evaluateJavaScript("window.location.replace('about:blank');")
311308
}
312309

313310
@objc
@@ -346,10 +343,6 @@ extension BrowserTab: WKNavigationDelegate {
346343

347344
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) {
348345
handleNavigationStart()
349-
if let webView = webView as? CustomWKWebView {
350-
webView.hoverListener = self
351-
self.statusBar?.label = ""
352-
}
353346
}
354347

355348
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation?, withError error: Error) {
@@ -384,20 +377,3 @@ extension BrowserTab: WKNavigationDelegate {
384377
self.navigationEndHandler.append(navigationEndHandler)
385378
}
386379
}
387-
388-
// MARK: Javascript messages handler
389-
390-
extension BrowserTab: WKScriptMessageHandler {
391-
392-
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
393-
if self.statusBar != nil {
394-
if message.name == CustomWKWebView.mouseDidEnterName {
395-
if let link = message.body as? String {
396-
self.statusBar?.label = link
397-
}
398-
} else if message.name == CustomWKWebView.mouseDidExitName {
399-
self.statusBar?.label = ""
400-
}
401-
}
402-
}
403-
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// CustomWKHoverUIDelegate.swift
3+
// Vienna
4+
//
5+
// Copyright 2023 Tassilo Karge
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// https://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
22+
//TODO: to me it looks like this protocol should be part of the CustomWKUIDelegate, but the unified display view does not declare conformance to that protocol. Probably that is a design flaw in how the unified display view integrates the browser.
23+
@objc protocol CustomWKHoverUIDelegate {
24+
@objc
25+
func hovered(link: String?)
26+
}

Vienna/Sources/Main window/CustomWKWebView.swift

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,9 @@ class CustomWKWebView: WKWebView {
2424
}
2525
}
2626

27-
@objc weak var hoverListener: WKScriptMessageHandler? {
27+
@objc weak var hoverUiDelegate: CustomWKHoverUIDelegate? {
2828
didSet {
29-
let contentController = self.configuration.userContentController
30-
contentController.removeScriptMessageHandler(forName: CustomWKWebView.mouseDidEnterName)
31-
contentController.removeScriptMessageHandler(forName: CustomWKWebView.mouseDidExitName)
32-
if let hoverListener = hoverListener {
33-
contentController.add(hoverListener, name: CustomWKWebView.mouseDidEnterName)
34-
contentController.add(hoverListener, name: CustomWKWebView.mouseDidExitName)
35-
}
29+
resetHoverUiListener()
3630
}
3731
}
3832

@@ -118,6 +112,18 @@ class CustomWKWebView: WKWebView {
118112
self.contextMenuListener = contextMenuListener
119113
contentController.add(contextMenuListener, name: CustomWKWebView.clickListenerName)
120114
contentController.add(CustomWKWebViewErrorListener(), name: CustomWKWebView.jsErrorListenerName)
115+
resetHoverUiListener()
116+
}
117+
118+
func resetHoverUiListener() {
119+
let contentController = self.configuration.userContentController
120+
contentController.removeScriptMessageHandler(forName: CustomWKWebView.mouseDidEnterName)
121+
contentController.removeScriptMessageHandler(forName: CustomWKWebView.mouseDidExitName)
122+
if let hoverUiDelegate = hoverUiDelegate {
123+
let hoverListener = CustomWKWebViewHoverListener(hoverDelegate: hoverUiDelegate)
124+
contentController.add(hoverListener, name: CustomWKWebView.mouseDidEnterName)
125+
contentController.add(hoverListener, name: CustomWKWebView.mouseDidExitName)
126+
}
121127
}
122128

123129
func search(_ text: String = "", upward: Bool = false) {
@@ -344,6 +350,28 @@ extension CustomWKWebView {
344350
}
345351
}
346352

353+
class CustomWKWebViewHoverListener: NSObject, WKScriptMessageHandler {
354+
355+
weak var hoverDelegate: CustomWKHoverUIDelegate?
356+
357+
init(hoverDelegate: CustomWKHoverUIDelegate) {
358+
self.hoverDelegate = hoverDelegate
359+
}
360+
361+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
362+
guard let hoverDelegate = hoverDelegate else {
363+
return
364+
}
365+
if message.name == CustomWKWebView.mouseDidEnterName {
366+
if let link = message.body as? String {
367+
hoverDelegate.hovered(link: link)
368+
}
369+
} else if message.name == CustomWKWebView.mouseDidExitName {
370+
hoverDelegate.hovered(link: nil)
371+
}
372+
}
373+
}
374+
347375
class CustomWKWebViewContextMenuListener: NSObject, WKScriptMessageHandler {
348376

349377
var lastRightClickedLink: URL?

Vienna/Sources/Main window/UnifiedDisplayView.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
@class AppController;
2626
@class ExtendedTableView;
2727

28-
@interface UnifiedDisplayView : NSView <BaseView, ArticleBaseView, NSMenuItemValidation, MessageListViewDelegate, NSTableViewDataSource, WKScriptMessageHandler>
28+
@interface UnifiedDisplayView : NSView <BaseView, ArticleBaseView, NSMenuItemValidation, MessageListViewDelegate, NSTableViewDataSource>
2929
{
3030
IBOutlet ExtendedTableView *articleList;
3131

Vienna/Sources/Main window/UnifiedDisplayView.m

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
#define YPOS_IN_CELL 2.0
4343
#define PROGRESS_INDICATOR_DIMENSION 16
4444

45-
@interface UnifiedDisplayView ()
45+
@interface UnifiedDisplayView () <CustomWKHoverUIDelegate>
4646

4747
@property (nonatomic) OverlayStatusBar *statusBar;
4848

@@ -766,7 +766,7 @@ - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn
766766
if ([articleContentView isKindOfClass:WebKitArticleView.class])
767767
{
768768
view = (WebKitArticleView *)articleContentView;
769-
((CustomWKWebView *)view).hoverListener = self;
769+
((CustomWKWebView *)view).hoverUiDelegate = self;
770770
} else {
771771
view = ((ArticleView *) articleContentView);
772772
}
@@ -1018,15 +1018,10 @@ - (void)observeValueForKeyPath:(NSString *)keyPath
10181018
}
10191019
}
10201020

1021-
// MARK: WKScriptMessageHandler
1021+
// MARK: CustomWKHoverUIDelegate
10221022

1023-
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
1024-
{
1025-
if ([message.name isEqualToString:CustomWKWebView.mouseDidEnterName]) {
1026-
NSString * link = (NSString *)message.body;
1027-
self.statusBar.label = link;
1028-
} else if ([message.name isEqualToString:CustomWKWebView.mouseDidExitName]) {
1029-
self.statusBar.label = nil;
1030-
}
1023+
-(void)hoveredWithLink:(NSString *)link {
1024+
self.statusBar.label = link;
10311025
}
1026+
10321027
@end

0 commit comments

Comments
 (0)