Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
[webview_flutter_wkwebview] Prevent leaking when a callback method re…
Browse files Browse the repository at this point in the history
…ferences an object that references itself (#6056)
  • Loading branch information
bparrishMines authored Jul 7, 2022
1 parent 1be7a6a commit 5e5dee8
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart';
import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart';
import 'package:webview_flutter_wkwebview_example/navigation_decision.dart';
import 'package:webview_flutter_wkwebview_example/navigation_request.dart';
import 'package:webview_flutter_wkwebview_example/web_view.dart';
Expand Down Expand Up @@ -66,6 +68,27 @@ Future<void> main() async {
expect(currentUrl, primaryUrl);
});

testWidgets(
'withWeakRefenceTo allows encapsulating class to be garbage collected',
(WidgetTester tester) async {
final Completer<int> gcCompleter = Completer<int>();
final InstanceManager instanceManager = InstanceManager(
onWeakReferenceRemoved: gcCompleter.complete,
);

ClassWithCallbackClass? instance = ClassWithCallbackClass();
instanceManager.addHostCreatedInstance(instance.callbackClass, 0);
instance = null;

// Force garbage collection.
await IntegrationTestWidgetsFlutterBinding.instance
.watchPerformance(() async {
await tester.pumpAndSettle();
});

expect(gcCompleter.future, completion(0));
}, timeout: const Timeout(Duration(seconds: 10)));

testWidgets('loadUrl', (WidgetTester tester) async {
final Completer<WebViewController> controllerCompleter =
Completer<WebViewController>();
Expand Down Expand Up @@ -1253,3 +1276,33 @@ class ResizableWebViewState extends State<ResizableWebView> {
);
}
}

class CopyableObjectWithCallback with Copyable {
CopyableObjectWithCallback(this.callback);

final VoidCallback callback;

@override
CopyableObjectWithCallback copy() {
return CopyableObjectWithCallback(callback);
}
}

class ClassWithCallbackClass {
ClassWithCallbackClass() {
callbackClass = CopyableObjectWithCallback(
withWeakRefenceTo(
this,
(WeakReference<ClassWithCallbackClass> weakReference) {
return () {
// Weak reference to `this` in callback.
// ignore: unnecessary_statements
weakReference;
};
},
),
);
}

late final CopyableObjectWithCallback callbackClass;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Helper method for creating callbacks methods with a weak reference.
///
/// Example:
/// ```
/// final JavascriptChannelRegistry javascriptChannelRegistry = ...
///
/// final WKScriptMessageHandler handler = WKScriptMessageHandler(
/// didReceiveScriptMessage: withWeakRefenceTo(
/// javascriptChannelRegistry,
/// (WeakReference<JavascriptChannelRegistry> weakReference) {
/// return (
/// WKUserContentController userContentController,
/// WKScriptMessage message,
/// ) {
/// weakReference.target?.onJavascriptChannelMessage(
/// message.name,
/// message.body!.toString(),
/// );
/// };
/// },
/// ),
/// );
/// ```
S withWeakRefenceTo<T extends Object, S extends Object>(
T reference,
S Function(WeakReference<T> weakReference) onCreate,
) {
final WeakReference<T> weakReference = WeakReference<T>(reference);
return onCreate(weakReference);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart';

import '../common/instance_manager.dart';
import 'foundation_api_impls.dart';
Expand Down Expand Up @@ -267,7 +268,19 @@ class NSObject with Copyable {

final NSObjectHostApiImpl _api;

/// Informs the observing object when the value at the specified key path has changed.
/// Informs the observing object when the value at the specified key path has
/// changed.
///
/// {@template webview_flutter_wkwebview.foundation.callbacks}
/// For the associated Objective-C object to be automatically garbage
/// collected, it is required that this Function doesn't contain a strong
/// reference to the encapsulating class instance. Consider using
/// `WeakReference` when referencing an object not received as a parameter.
/// Otherwise, use [NSObject.dispose] to release the associated Objective-C
/// object manually.
///
/// See [withWeakRefenceTo].
/// {@endtemplate}
final void Function(
String keyPath,
NSObject object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ class WKScriptMessageHandler extends NSObject {
/// Use this method to respond to a message sent from the webpage’s
/// JavaScript code. Use the [message] parameter to get the message contents and
/// to determine the originating web view.
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final void Function(
WKUserContentController userContentController,
WKScriptMessage message,
Expand Down Expand Up @@ -733,6 +735,8 @@ class WKUIDelegate extends NSObject {
final WKUIDelegateHostApiImpl _uiDelegateApi;

/// Indicates a new [WKWebView] was requested to be created with [configuration].
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final void Function(
WKWebView webView,
WKWebViewConfiguration configuration,
Expand Down Expand Up @@ -803,26 +807,38 @@ class WKNavigationDelegate extends NSObject {
final WKNavigationDelegateHostApiImpl _navigationDelegateApi;

/// Called when navigation is complete.
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final void Function(WKWebView webView, String? url)? didFinishNavigation;

/// Called when navigation from the main frame has started.
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final void Function(WKWebView webView, String? url)?
didStartProvisionalNavigation;

/// Called when permission is needed to navigate to new content.
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final Future<WKNavigationActionPolicy> Function(
WKWebView webView,
WKNavigationAction navigationAction,
)? decidePolicyForNavigationAction;

/// Called when an error occurred during navigation.
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final void Function(WKWebView webView, NSError error)? didFailNavigation;

/// Called when an error occurred during the early navigation process.
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final void Function(WKWebView webView, NSError error)?
didFailProvisionalNavigation;

/// Called when the web view’s content process was terminated.
///
/// {@macro webview_flutter_wkwebview.foundation.callbacks}
final void Function(WKWebView webView)? webViewWebContentProcessDidTerminate;

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';

import 'common/weak_reference_utils.dart';
import 'foundation/foundation.dart';
import 'web_kit/web_kit.dart';

Expand Down Expand Up @@ -114,58 +115,74 @@ class WebKitWebViewPlatformController extends WebViewPlatformController {

/// Used to integrate custom user interface elements into web view interactions.
@visibleForTesting
late final WKUIDelegate uiDelegate =
webViewProxy.createUIDelgate(onCreateWebView: (
WKWebView webView,
WKWebViewConfiguration configuration,
WKNavigationAction navigationAction,
) {
if (!navigationAction.targetFrame.isMainFrame) {
webView.loadRequest(navigationAction.request);
}
});
late final WKUIDelegate uiDelegate = webViewProxy.createUIDelgate(
onCreateWebView: (
WKWebView webView,
WKWebViewConfiguration configuration,
WKNavigationAction navigationAction,
) {
if (!navigationAction.targetFrame.isMainFrame) {
webView.loadRequest(navigationAction.request);
}
},
);

/// Methods for handling navigation changes and tracking navigation requests.
@visibleForTesting
late final WKNavigationDelegate navigationDelegate =
webViewProxy.createNavigationDelegate(
didFinishNavigation: (WKWebView webView, String? url) {
callbacksHandler.onPageFinished(url ?? '');
},
didStartProvisionalNavigation: (WKWebView webView, String? url) {
callbacksHandler.onPageStarted(url ?? '');
},
decidePolicyForNavigationAction: (
WKWebView webView,
WKNavigationAction action,
) async {
if (!_hasNavigationDelegate) {
return WKNavigationActionPolicy.allow;
}
late final WKNavigationDelegate navigationDelegate = withWeakRefenceTo(
this,
(WeakReference<WebKitWebViewPlatformController> weakReference) {
return webViewProxy.createNavigationDelegate(
didFinishNavigation: (WKWebView webView, String? url) {
weakReference.target?.callbacksHandler.onPageFinished(url ?? '');
},
didStartProvisionalNavigation: (WKWebView webView, String? url) {
weakReference.target?.callbacksHandler.onPageStarted(url ?? '');
},
decidePolicyForNavigationAction: (
WKWebView webView,
WKNavigationAction action,
) async {
if (weakReference.target == null) {
return WKNavigationActionPolicy.allow;
}

if (!weakReference.target!._hasNavigationDelegate) {
return WKNavigationActionPolicy.allow;
}

final bool allow =
await weakReference.target!.callbacksHandler.onNavigationRequest(
url: action.request.url,
isForMainFrame: action.targetFrame.isMainFrame,
);

final bool allow = await callbacksHandler.onNavigationRequest(
url: action.request.url,
isForMainFrame: action.targetFrame.isMainFrame,
return allow
? WKNavigationActionPolicy.allow
: WKNavigationActionPolicy.cancel;
},
didFailNavigation: (WKWebView webView, NSError error) {
weakReference.target?.callbacksHandler.onWebResourceError(
_toWebResourceError(error),
);
},
didFailProvisionalNavigation: (WKWebView webView, NSError error) {
weakReference.target?.callbacksHandler.onWebResourceError(
_toWebResourceError(error),
);
},
webViewWebContentProcessDidTerminate: (WKWebView webView) {
weakReference.target?.callbacksHandler.onWebResourceError(
WebResourceError(
errorCode: WKErrorCode.webContentProcessTerminated,
// Value from https://developer.apple.com/documentation/webkit/wkerrordomain?language=objc.
domain: 'WKErrorDomain',
description: '',
errorType: WebResourceErrorType.webContentProcessTerminated,
),
);
},
);

return allow
? WKNavigationActionPolicy.allow
: WKNavigationActionPolicy.cancel;
},
didFailNavigation: (WKWebView webView, NSError error) {
callbacksHandler.onWebResourceError(_toWebResourceError(error));
},
didFailProvisionalNavigation: (WKWebView webView, NSError error) {
callbacksHandler.onWebResourceError(_toWebResourceError(error));
},
webViewWebContentProcessDidTerminate: (WKWebView webView) {
callbacksHandler.onWebResourceError(WebResourceError(
errorCode: WKErrorCode.webContentProcessTerminated,
// Value from https://developer.apple.com/documentation/webkit/wkerrordomain?language=objc.
domain: 'WKErrorDomain',
description: '',
errorType: WebResourceErrorType.webContentProcessTerminated,
));
},
);

Expand All @@ -179,14 +196,23 @@ class WebKitWebViewPlatformController extends WebViewPlatformController {
autoMediaPlaybackPolicy: params.autoMediaPlaybackPolicy,
);

webView = webViewProxy.createWebView(configuration, observeValue: (
String keyPath,
NSObject object,
Map<NSKeyValueChangeKey, Object?> change,
) {
final double progress = change[NSKeyValueChangeKey.newValue]! as double;
callbacksHandler.onProgress((progress * 100).round());
});
webView = webViewProxy.createWebView(
configuration,
observeValue: withWeakRefenceTo(
callbacksHandler,
(WeakReference<WebViewPlatformCallbacksHandler> weakReference) {
return (
String keyPath,
NSObject object,
Map<NSKeyValueChangeKey, Object?> change,
) {
final double progress =
change[NSKeyValueChangeKey.newValue]! as double;
weakReference.target?.onProgress((progress * 100).round());
};
},
),
);

webView.setUIDelegate(uiDelegate);

Expand Down Expand Up @@ -413,15 +439,22 @@ class WebKitWebViewPlatformController extends WebViewPlatformController {
).map<Future<void>>(
(String channelName) {
final WKScriptMessageHandler handler =
webViewProxy.createScriptMessageHandler(didReceiveScriptMessage: (
WKUserContentController userContentController,
WKScriptMessage message,
) {
javascriptChannelRegistry.onJavascriptChannelMessage(
message.name,
message.body!.toString(),
);
});
webViewProxy.createScriptMessageHandler(
didReceiveScriptMessage: withWeakRefenceTo(
javascriptChannelRegistry,
(WeakReference<JavascriptChannelRegistry> weakReference) {
return (
WKUserContentController userContentController,
WKScriptMessage message,
) {
weakReference.target?.onJavascriptChannelMessage(
message.name,
message.body!.toString(),
);
};
},
),
);
_scriptMessageHandlers[channelName] = handler;

final String wrapperSource =
Expand Down Expand Up @@ -652,11 +685,7 @@ class WebViewWidgetProxy {

/// Constructs a [WKNavigationDelegate].
WKNavigationDelegate createNavigationDelegate({
void Function(
WKWebView webView,
String? url,
)?
didFinishNavigation,
void Function(WKWebView webView, String? url)? didFinishNavigation,
void Function(WKWebView webView, String? url)?
didStartProvisionalNavigation,
Future<WKNavigationActionPolicy> Function(
Expand Down

0 comments on commit 5e5dee8

Please sign in to comment.