Skip to content
This repository was archived by the owner on May 1, 2024. It is now read-only.

Commit c80a908

Browse files
t-johnsonPureWeen
andauthored
Add ability to set WebView ExecutionMode property (addresses #4720) (#12509)
* To address issue #4720: Add option for custom renderers to control the execution mode of the WebView control This is 'opt-in' as by default this commit will not change behavior of existing applciations. to opt-in, people would need to set the ExecutionMode property in the constructor of their custom WebViewRenderer, like: public class MyWebViewRenderer : WebViewRenderer { public MyWebViewRenderer() { ExecutionMode = Windows.UI.Xaml.Controls.WebViewExecutionMode.SeparateProcess; } } When set as 'SeparateProcess', the memory allocated by the WebView itself is all handled in a sub-process, which ensures the main process never crashes from running out of memory here from doing things like opening and closing youtube every 5 seconds. This behavior will likely crash when the WebView is in-process due to the huge amounts of memory that is required by that website. * tabs not spaces * TemplatedItemsList: Ensure items are unhooked correctly when removing them, whether as individual removals or list resets. Without this, the native cells do not actually get removed. CellTableViewCell: use event PropertyChangedEventHandler instead of action (seems more standard) TextCellRenderer: Correct the event delegate hookup ListViewRenderer: Actually re-use the UITableViewCell when creating header sections, otherwise we endlessly create new ones, leaving the old ones alive through event handlers * Revert "TemplatedItemsList: Ensure items are unhooked correctly when removing them, whether as individual removals or list resets. Without this, the native cells do not actually get removed." This reverts commit 7da9ffb. * Make the custom renderer only apply to the test for this issue, as it seems the SeparateProcess affects access to cookies, breaking other tests * Move additional classes inside test class to decrease namespace cluttering. * - cleanup and rebase * - add instructions * - move WebViewExecutionMode to platform specific * - fix up UI Test * - clean up tests * - fix tabs * - fix formatting * - fix teardown call * - fix async ui test quirk on uwp Co-authored-by: Shane Neuville <shneuvil@microsoft.com>
1 parent dddcf66 commit c80a908

File tree

5 files changed

+274
-19
lines changed

5 files changed

+274
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using Xamarin.Forms.CustomAttributes;
2+
using Xamarin.Forms.Internals;
3+
using System.Threading;
4+
using System.Diagnostics;
5+
using System;
6+
using System.Threading.Tasks;
7+
using Xamarin.Forms.Controls.GalleryPages;
8+
using Xamarin.Forms.PlatformConfiguration.WindowsSpecific;
9+
10+
#if UITEST
11+
using Xamarin.Forms.Core.UITests;
12+
using Xamarin.UITest;
13+
using NUnit.Framework;
14+
#endif
15+
16+
namespace Xamarin.Forms.Controls.Issues
17+
{
18+
#if UITEST
19+
[Category(UITestCategories.WebView)]
20+
#endif
21+
[Preserve(AllMembers = true)]
22+
[Issue(IssueTracker.Github, 4720, "UWP: Webview: Memory Leak in WebView", PlatformAffected.UWP)]
23+
public class Issue4720 : TestNavigationPage
24+
{
25+
protected override void Init()
26+
{
27+
PushAsync(new Issue4720Content());
28+
}
29+
30+
#if UITEST
31+
protected override bool Isolate => true;
32+
33+
[Test]
34+
public void WebViewDoesntCrashWhenLoadingAHeavyPageAndUsingExecutionModeSeparateProcess()
35+
{
36+
//4 iterations were enough to run out of memory before the fix.
37+
int iterations = 10;
38+
39+
for (int n = 0; n < iterations; n++)
40+
{
41+
RunningApp.WaitForElement(q => q.Marked("New Page"));
42+
RunningApp.Tap(q => q.Marked("New Page"));
43+
RunningApp.WaitForElement(q => q.Marked("Close Page"));
44+
Thread.Sleep(250);
45+
RunningApp.Tap(q => q.Marked("Close Page"));
46+
}
47+
RunningApp.Tap(q => q.Marked("GC"));
48+
}
49+
50+
#endif
51+
52+
[Preserve(AllMembers = true)]
53+
public class Issue4720WebPage : ContentPage
54+
{
55+
static int s_count;
56+
WebView _webView;
57+
58+
public Issue4720WebPage()
59+
{
60+
Interlocked.Increment(ref s_count);
61+
Debug.WriteLine($"++++++++ Issue4720WebPage : Constructor, count is {s_count}");
62+
63+
var label = new Label { Text = "Test case for GitHub issue #4720." };
64+
65+
var button = new Button { Text = "Close Page" };
66+
button.Clicked += ClosePageClicked;
67+
68+
69+
var btnChangeExecutionMode = new Button { Text = "Change Execution Mode" };
70+
btnChangeExecutionMode.Clicked += ChangeExecutionModeClicked;
71+
72+
_webView = new WebView()
73+
{
74+
Source = new UrlWebViewSource { Url = "https://www.microsoft.com/" },
75+
HorizontalOptions = LayoutOptions.FillAndExpand,
76+
VerticalOptions = LayoutOptions.FillAndExpand,
77+
BackgroundColor = Color.Red
78+
79+
};
80+
81+
_webView.On<PlatformConfiguration.Windows>().SetExecutionMode(WebViewExecutionMode.SeparateProcess);
82+
83+
Content = new StackLayout { Children = { label, button, btnChangeExecutionMode, _webView } };
84+
}
85+
86+
async void ClosePageClicked(object sender, EventArgs e)
87+
{
88+
await Navigation.PopAsync();
89+
}
90+
91+
void ChangeExecutionModeClicked(object sender, EventArgs e)
92+
{
93+
if(_webView.On<PlatformConfiguration.Windows>().GetExecutionMode() == WebViewExecutionMode.SameThread)
94+
_webView.On<PlatformConfiguration.Windows>().SetExecutionMode(WebViewExecutionMode.SeparateProcess);
95+
else
96+
_webView.On<PlatformConfiguration.Windows>().SetExecutionMode(WebViewExecutionMode.SameThread);
97+
}
98+
99+
~Issue4720WebPage()
100+
{
101+
Interlocked.Decrement(ref s_count);
102+
Debug.WriteLine($"-------- Issue4720WebPage: Destructor, count is {s_count}");
103+
}
104+
}
105+
106+
[Preserve(AllMembers = true)]
107+
public class Issue4720Content : ContentPage
108+
{
109+
static int s_count;
110+
111+
public Issue4720Content()
112+
{
113+
Interlocked.Increment(ref s_count);
114+
Debug.WriteLine($">>>>> Issue4720Content Issue4720Content : Constructor, count is {s_count}");
115+
116+
var button = new Button { Text = "New Page" };
117+
button.Clicked += Button_Clicked;
118+
119+
var gcbutton = new Button { Text = "GC" };
120+
gcbutton.Clicked += GCbutton_Clicked;
121+
122+
var instructions = new Label() { Text = "Navigate forward and back multiple times. If you don't see any out of memory exceptions the test has passed." };
123+
Content = new StackLayout { Children = { button, gcbutton, instructions } };
124+
}
125+
126+
void GCbutton_Clicked(object sender, EventArgs e)
127+
{
128+
System.Diagnostics.Debug.WriteLine(">>>>>>>> Running Garbage Collection");
129+
GarbageCollectionHelper.Collect();
130+
System.Diagnostics.Debug.WriteLine($">>>>>>>> GC.GetTotalMemory = {GC.GetTotalMemory(true):n0}");
131+
}
132+
133+
void Button_Clicked(object sender, EventArgs e)
134+
{
135+
Navigation.PushAsync(new Issue4720WebPage());
136+
}
137+
138+
~Issue4720Content()
139+
{
140+
Interlocked.Decrement(ref s_count);
141+
Debug.WriteLine($">>>>> Issue4720Content ~Issue4720Content : Destructor, count is {s_count}");
142+
}
143+
}
144+
}
145+
}

Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
</PropertyGroup>
1111
<ItemGroup>
1212
<Compile Include="$(MSBuildThisFileDirectory)CollectionViewGroupTypeIssue.cs" />
13+
<Compile Include="$(MSBuildThisFileDirectory)Issue4720.cs" />
1314
<Compile Include="$(MSBuildThisFileDirectory)Issue10897.xaml.cs">
1415
<DependentUpon>Issue10897.xaml</DependentUpon>
1516
</Compile>

Xamarin.Forms.Core/PlatformConfiguration/WindowsSpecific/WebView.cs

+25
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,30 @@ public static IPlatformElementConfiguration<Windows, FormsElement> SetIsJavaScri
2727
SetIsJavaScriptAlertEnabled(config.Element, value);
2828
return config;
2929
}
30+
31+
32+
33+
public static readonly BindableProperty ExecutionModeProperty = BindableProperty.Create("ExecutionMode", typeof(WebViewExecutionMode), typeof(WebView), WebViewExecutionMode.SameThread);
34+
35+
public static WebViewExecutionMode GetExecutionMode(BindableObject element)
36+
{
37+
return (WebViewExecutionMode)element.GetValue(ExecutionModeProperty);
38+
}
39+
40+
public static void SetExecutionMode(BindableObject element, WebViewExecutionMode value)
41+
{
42+
element.SetValue(ExecutionModeProperty, value);
43+
}
44+
45+
public static WebViewExecutionMode GetExecutionMode(this IPlatformElementConfiguration<Windows, FormsElement> config)
46+
{
47+
return GetExecutionMode(config.Element);
48+
}
49+
50+
public static IPlatformElementConfiguration<Windows, FormsElement> SetExecutionMode(this IPlatformElementConfiguration<Windows, FormsElement> config, WebViewExecutionMode value)
51+
{
52+
SetExecutionMode(config.Element, value);
53+
return config;
54+
}
3055
}
3156
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace Xamarin.Forms.PlatformConfiguration.WindowsSpecific
6+
{
7+
public enum WebViewExecutionMode
8+
{
9+
SameThread = 0,
10+
SeparateThread = 1,
11+
SeparateProcess = 2
12+
}
13+
}

Xamarin.Forms.Platform.UAP/WebViewRenderer.cs

+90-19
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
using Windows.Web.Http;
1111
using System.Collections.Generic;
1212
using System.Linq;
13-
13+
using WWebView = Windows.UI.Xaml.Controls.WebView;
14+
using WWebViewExecutionMode = Windows.UI.Xaml.Controls.WebViewExecutionMode;
1415
namespace Xamarin.Forms.Platform.UWP
1516
{
16-
public class WebViewRenderer : ViewRenderer<WebView, Windows.UI.Xaml.Controls.WebView>, IWebViewDelegate
17+
public class WebViewRenderer : ViewRenderer<WebView, WWebView>, IWebViewDelegate
1718
{
1819
WebNavigationEvent _eventState;
1920
bool _updating;
20-
Windows.UI.Xaml.Controls.WebView _internalWebView;
21+
WWebView _internalWebView;
2122
const string LocalScheme = "ms-appx-web:///";
2223

2324
// Script to insert a <base> tag into an HTML document
@@ -41,7 +42,7 @@ public void LoadHtml(string html, string baseUrl)
4142

4243
// Set up an internal WebView we can use to load and parse the original HTML string
4344
// Make _internalWebView a field instead of local variable to avoid garbage collection
44-
_internalWebView = new Windows.UI.Xaml.Controls.WebView();
45+
_internalWebView = new WWebView();
4546

4647
// When the 'navigation' to the original HTML string is done, we can modify it to include our <base> tag
4748
_internalWebView.NavigationCompleted += async (sender, args) =>
@@ -101,22 +102,77 @@ public void LoadUrl(string url)
101102
}
102103
}
103104

105+
void TearDown(WWebView webView)
106+
{
107+
if (webView == null)
108+
{
109+
return;
110+
}
111+
webView.SeparateProcessLost -= OnSeparateProcessLost;
112+
webView.NavigationStarting -= OnNavigationStarted;
113+
webView.NavigationCompleted -= OnNavigationCompleted;
114+
webView.NavigationFailed -= OnNavigationFailed;
115+
webView.ScriptNotify -= OnScriptNotify;
116+
}
117+
118+
void Connect(WWebView webView)
119+
{
120+
if (webView == null)
121+
{
122+
return;
123+
}
124+
125+
webView.SeparateProcessLost += OnSeparateProcessLost;
126+
webView.NavigationStarting += OnNavigationStarted;
127+
webView.NavigationCompleted += OnNavigationCompleted;
128+
webView.NavigationFailed += OnNavigationFailed;
129+
webView.ScriptNotify += OnScriptNotify;
130+
}
131+
104132
protected override void Dispose(bool disposing)
105133
{
106134
if (disposing)
107135
{
108-
if (Control != null)
136+
TearDown(Control);
137+
if (Element != null)
109138
{
110-
Control.NavigationStarting -= OnNavigationStarted;
111-
Control.NavigationCompleted -= OnNavigationCompleted;
112-
Control.NavigationFailed -= OnNavigationFailed;
113-
Control.ScriptNotify -= OnScriptNotify;
139+
Element.EvalRequested -= OnEvalRequested;
140+
Element.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
141+
Element.GoBackRequested -= OnGoBackRequested;
142+
Element.GoForwardRequested -= OnGoForwardRequested;
143+
Element.ReloadRequested -= OnReloadRequested;
114144
}
115145
}
116146

117147
base.Dispose(disposing);
118148
}
119149

150+
protected virtual WWebView CreateNativeControl()
151+
{
152+
if (Element.IsSet(PlatformConfiguration.WindowsSpecific.WebView.ExecutionModeProperty))
153+
{
154+
WWebViewExecutionMode webViewExecutionMode = WWebViewExecutionMode.SameThread;
155+
156+
switch (Element.OnThisPlatform().GetExecutionMode())
157+
{
158+
case PlatformConfiguration.WindowsSpecific.WebViewExecutionMode.SameThread:
159+
webViewExecutionMode = WWebViewExecutionMode.SameThread;
160+
break;
161+
case PlatformConfiguration.WindowsSpecific.WebViewExecutionMode.SeparateProcess:
162+
webViewExecutionMode = WWebViewExecutionMode.SeparateProcess;
163+
break;
164+
case PlatformConfiguration.WindowsSpecific.WebViewExecutionMode.SeparateThread:
165+
webViewExecutionMode = WWebViewExecutionMode.SeparateThread;
166+
break;
167+
168+
}
169+
170+
return new WWebView(webViewExecutionMode);
171+
}
172+
173+
return new WWebView();
174+
}
175+
120176
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
121177
{
122178
base.OnElementChanged(e);
@@ -135,11 +191,8 @@ protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
135191
{
136192
if (Control == null)
137193
{
138-
var webView = new Windows.UI.Xaml.Controls.WebView();
139-
webView.NavigationStarting += OnNavigationStarted;
140-
webView.NavigationCompleted += OnNavigationCompleted;
141-
webView.NavigationFailed += OnNavigationFailed;
142-
webView.ScriptNotify += OnScriptNotify;
194+
var webView = CreateNativeControl();
195+
Connect(webView);
143196
SetNativeControl(webView);
144197
}
145198

@@ -163,6 +216,10 @@ protected override void OnElementPropertyChanged(object sender, PropertyChangedE
163216
if (!_updating)
164217
Load();
165218
}
219+
else if (e.Is(PlatformConfiguration.WindowsSpecific.WebView.ExecutionModeProperty))
220+
{
221+
UpdateExecutionMode();
222+
}
166223
}
167224

168225
HashSet<string> _loadedCookies = new HashSet<string>();
@@ -192,7 +249,7 @@ HttpCookieCollection GetCookiesFromNativeStore(string url)
192249
{
193250
var uri = CreateUriForCookies(url);
194251
CookieContainer existingCookies = new CookieContainer();
195-
var filter = new Windows.Web.Http.Filters.HttpBaseProtocolFilter();
252+
var filter = new Windows.Web.Http.Filters.HttpBaseProtocolFilter();
196253
var nativeCookies = filter.CookieManager.GetCookies(uri);
197254
return nativeCookies;
198255
}
@@ -296,14 +353,14 @@ void Load()
296353

297354
async void OnEvalRequested(object sender, EvalRequested eventArg)
298355
{
299-
await Control.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
356+
await Control.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
300357
async () =>
301358
{
302359
try
303360
{
304361
await Control.InvokeScriptAsync("eval", new[] { eventArg.Script });
305362
}
306-
catch(Exception exc)
363+
catch (Exception exc)
307364
{
308365
Log.Warning(nameof(WebView), $"Eval of script failed: {exc} Script: {eventArg.Script}");
309366
}
@@ -343,7 +400,7 @@ void OnReloadRequested(object sender, EventArgs eventArgs)
343400
Control.Refresh();
344401
}
345402

346-
async void OnNavigationCompleted(Windows.UI.Xaml.Controls.WebView sender, WebViewNavigationCompletedEventArgs e)
403+
async void OnNavigationCompleted(WWebView sender, WebViewNavigationCompletedEventArgs e)
347404
{
348405
if (e.Uri != null)
349406
SendNavigated(new UrlWebViewSource { Url = e.Uri.AbsoluteUri }, _eventState, WebNavigationResult.Success);
@@ -360,7 +417,7 @@ void OnNavigationFailed(object sender, WebViewNavigationFailedEventArgs e)
360417
SendNavigated(new UrlWebViewSource { Url = e.Uri.AbsoluteUri }, _eventState, WebNavigationResult.Failure);
361418
}
362419

363-
void OnNavigationStarted(Windows.UI.Xaml.Controls.WebView sender, WebViewNavigationStartingEventArgs e)
420+
void OnNavigationStarted(WWebView sender, WebViewNavigationStartingEventArgs e)
364421
{
365422
Uri uri = e.Uri;
366423

@@ -401,5 +458,19 @@ void UpdateCanGoBackForward()
401458
((IWebViewController)Element).CanGoBack = Control.CanGoBack;
402459
((IWebViewController)Element).CanGoForward = Control.CanGoForward;
403460
}
461+
462+
void UpdateExecutionMode()
463+
{
464+
TearDown(Control);
465+
var webView = CreateNativeControl();
466+
Connect(webView);
467+
SetNativeControl(webView);
468+
Load();
469+
}
470+
471+
void OnSeparateProcessLost(WWebView sender, WebViewSeparateProcessLostEventArgs e)
472+
{
473+
UpdateExecutionMode();
474+
}
404475
}
405476
}

0 commit comments

Comments
 (0)