Fix memory leaks in MAUI views (#2955)

Fixes #2923
This commit is contained in:
Matthew Leibowitz 2024-07-26 16:14:49 +08:00 коммит произвёл GitHub
Родитель d3a6ae9491
Коммит ccde024b62
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
13 изменённых файлов: 599 добавлений и 136 удалений

Просмотреть файл

@ -1,7 +1,7 @@
#nullable enable
using System;
using System.ComponentModel;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
@ -10,6 +10,9 @@ namespace SkiaSharp.Views.Maui.Controls
{
public partial class SKGLView : View, ISKGLView
{
private static readonly BindableProperty ProxyWindowProperty =
BindableProperty.Create("ProxyWindow", typeof(Window), typeof(SKGLView), propertyChanged: OnWindowChanged);
public static readonly BindableProperty IgnorePixelScalingProperty =
BindableProperty.Create(nameof(IgnorePixelScaling), typeof(bool), typeof(SKGLView), false);
@ -22,6 +25,12 @@ namespace SkiaSharp.Views.Maui.Controls
private SKSizeI lastCanvasSize;
private GRContext? lastGRContext;
public SKGLView()
{
var binding = new Binding(nameof(Window), source: this);
SetBinding(ProxyWindowProperty, binding);
}
public bool IgnorePixelScaling
{
get => (bool)GetValue(IgnorePixelScalingProperty);
@ -63,6 +72,17 @@ namespace SkiaSharp.Views.Maui.Controls
Touch?.Invoke(this, e);
}
private static void OnWindowChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is not SKGLView view)
return;
view.Handler?.UpdateValue(nameof(HasRenderLoop));
}
bool ISKGLView.HasRenderLoop =>
HasRenderLoop && Window is not null;
void ISKGLView.OnCanvasSizeChanged(SKSizeI size) =>
lastCanvasSize = size;

Просмотреть файл

@ -7,24 +7,27 @@ namespace SkiaSharp.Views.Maui.Handlers
{
public partial class SKCanvasViewHandler : ViewHandler<ISKCanvasView, SKCanvasView>
{
private SKSizeI lastCanvasSize;
private SKTouchHandler? touchHandler;
private PaintSurfaceProxy? paintSurfaceProxy;
private SKTouchHandlerProxy? touchProxy;
protected override SKCanvasView CreatePlatformView() => new SKCanvasView { BackgroundColor = UIColor.Clear };
protected override void ConnectHandler(SKCanvasView platformView)
{
platformView.PaintSurface += OnPaintSurface;
paintSurfaceProxy = new();
paintSurfaceProxy.Connect(VirtualView, platformView);
touchProxy = new();
touchProxy.Connect(VirtualView, platformView);
base.ConnectHandler(platformView);
}
protected override void DisconnectHandler(SKCanvasView platformView)
{
touchHandler?.Detach(platformView);
touchHandler = null;
platformView.PaintSurface -= OnPaintSurface;
paintSurfaceProxy?.Disconnect(platformView);
paintSurfaceProxy = null;
touchProxy?.Disconnect(platformView);
touchProxy = null;
base.DisconnectHandler(platformView);
}
@ -43,38 +46,35 @@ namespace SkiaSharp.Views.Maui.Handlers
public static void MapEnableTouchEvents(SKCanvasViewHandler handler, ISKCanvasView canvasView)
{
handler.touchHandler ??= new SKTouchHandler(
args => canvasView.OnTouch(args),
(x, y) => handler.OnGetScaledCoord(x, y));
handler.touchHandler?.SetEnabled(handler.PlatformView, canvasView.EnableTouchEvents);
handler.touchProxy?.UpdateEnableTouchEvents(handler.PlatformView, canvasView.EnableTouchEvents);
}
// helper methods
private void OnPaintSurface(object? sender, iOS.SKPaintSurfaceEventArgs e)
private class PaintSurfaceProxy : SKEventProxy<ISKCanvasView, SKCanvasView>
{
var newCanvasSize = e.Info.Size;
if (lastCanvasSize != newCanvasSize)
private SKSizeI lastCanvasSize;
protected override void OnConnect(ISKCanvasView virtualView, SKCanvasView platformView) =>
platformView.PaintSurface += OnPaintSurface;
protected override void OnDisconnect(SKCanvasView platformView) =>
platformView.PaintSurface -= OnPaintSurface;
private void OnPaintSurface(object? sender, iOS.SKPaintSurfaceEventArgs e)
{
lastCanvasSize = newCanvasSize;
VirtualView?.OnCanvasSizeChanged(newCanvasSize);
if (VirtualView is not {} view)
return;
var newCanvasSize = e.Info.Size;
if (lastCanvasSize != newCanvasSize)
{
lastCanvasSize = newCanvasSize;
view.OnCanvasSizeChanged(newCanvasSize);
}
view.OnPaintSurface(new SKPaintSurfaceEventArgs(e.Surface, e.Info, e.RawInfo));
}
VirtualView?.OnPaintSurface(new SKPaintSurfaceEventArgs(e.Surface, e.Info, e.RawInfo));
}
private SKPoint OnGetScaledCoord(double x, double y)
{
if (VirtualView?.IgnorePixelScaling == false && PlatformView != null)
{
var scale = PlatformView.ContentScaleFactor;
x *= scale;
y *= scale;
}
return new SKPoint((float)x, (float)y);
}
}
}

Просмотреть файл

@ -8,9 +8,8 @@ namespace SkiaSharp.Views.Maui.Handlers
{
public partial class SKGLViewHandler : ViewHandler<ISKGLView, SKMetalView>
{
private SKSizeI lastCanvasSize;
private GRContext? lastGRContext;
private SKTouchHandler? touchHandler;
private PaintSurfaceProxy? paintSurfaceProxy;
private SKTouchHandlerProxy? touchProxy;
protected override SKMetalView CreatePlatformView() =>
new MauiSKMetalView
@ -21,17 +20,20 @@ namespace SkiaSharp.Views.Maui.Handlers
protected override void ConnectHandler(SKMetalView platformView)
{
platformView.PaintSurface += OnPaintSurface;
paintSurfaceProxy = new();
paintSurfaceProxy.Connect(VirtualView, platformView);
touchProxy = new();
touchProxy.Connect(VirtualView, platformView);
base.ConnectHandler(platformView);
}
protected override void DisconnectHandler(SKMetalView platformView)
{
touchHandler?.Detach(platformView);
touchHandler = null;
platformView.PaintSurface -= OnPaintSurface;
paintSurfaceProxy?.Disconnect(platformView);
paintSurfaceProxy = null;
touchProxy?.Disconnect(platformView);
touchProxy = null;
base.DisconnectHandler(platformView);
}
@ -61,49 +63,11 @@ namespace SkiaSharp.Views.Maui.Handlers
public static void MapEnableTouchEvents(SKGLViewHandler handler, ISKGLView view)
{
handler.touchHandler ??= new SKTouchHandler(
args => view.OnTouch(args),
(x, y) => handler.OnGetScaledCoord(x, y));
handler.touchHandler?.SetEnabled(handler.PlatformView, view.EnableTouchEvents);
handler.touchProxy?.UpdateEnableTouchEvents(handler.PlatformView, view.EnableTouchEvents);
}
// helper methods
private void OnPaintSurface(object? sender, iOS.SKPaintMetalSurfaceEventArgs e)
{
var newCanvasSize = e.Info.Size;
if (lastCanvasSize != newCanvasSize)
{
lastCanvasSize = newCanvasSize;
VirtualView?.OnCanvasSizeChanged(newCanvasSize);
}
if (sender is SKMetalView platformView)
{
var newGRContext = platformView.GRContext;
if (lastGRContext != newGRContext)
{
lastGRContext = newGRContext;
VirtualView?.OnGRContextChanged(newGRContext);
}
}
VirtualView?.OnPaintSurface(new SKPaintGLSurfaceEventArgs(e.Surface, e.BackendRenderTarget, e.Origin, e.Info, e.RawInfo));
}
private SKPoint OnGetScaledCoord(double x, double y)
{
if (VirtualView?.IgnorePixelScaling == false && PlatformView != null)
{
var scale = PlatformView.ContentScaleFactor;
x *= scale;
y *= scale;
}
return new SKPoint((float)x, (float)y);
}
private class MauiSKMetalView : SKMetalView
{
public bool IgnorePixelScaling { get; set; }
@ -123,5 +87,41 @@ namespace SkiaSharp.Views.Maui.Handlers
base.OnPaintSurface(e);
}
}
private class PaintSurfaceProxy : SKEventProxy<ISKGLView, SKMetalView>
{
private SKSizeI lastCanvasSize;
private GRContext? lastGRContext;
protected override void OnConnect(ISKGLView virtualView, SKMetalView platformView) =>
platformView.PaintSurface += OnPaintSurface;
protected override void OnDisconnect(SKMetalView platformView) =>
platformView.PaintSurface -= OnPaintSurface;
private void OnPaintSurface(object? sender, iOS.SKPaintMetalSurfaceEventArgs e)
{
if (VirtualView is not {} view)
return;
var newCanvasSize = e.Info.Size;
if (lastCanvasSize != newCanvasSize)
{
lastCanvasSize = newCanvasSize;
view.OnCanvasSizeChanged(newCanvasSize);
}
if (sender is SKMetalView platformView)
{
var newGRContext = platformView.GRContext;
if (lastGRContext != newGRContext)
{
lastGRContext = newGRContext;
view.OnGRContextChanged(newGRContext);
}
}
view.OnPaintSurface(new SKPaintGLSurfaceEventArgs(e.Surface, e.BackendRenderTarget, e.Origin, e.Info, e.RawInfo));
}
}
}
}

Просмотреть файл

@ -16,9 +16,8 @@ namespace SkiaSharp.Views.Maui.Handlers
[UnsupportedOSPlatform("macos")]
public partial class SKGLViewHandler : ViewHandler<ISKGLView, SKGLView>
{
private SKSizeI lastCanvasSize;
private GRContext? lastGRContext;
private SKTouchHandler? touchHandler;
private PaintSurfaceProxy? paintSurfaceProxy;
private SKTouchHandlerProxy? touchProxy;
private RenderLoopManager? renderLoopManager;
protected override SKGLView CreatePlatformView() =>
@ -30,22 +29,23 @@ namespace SkiaSharp.Views.Maui.Handlers
protected override void ConnectHandler(SKGLView platformView)
{
paintSurfaceProxy = new();
paintSurfaceProxy.Connect(VirtualView, platformView);
touchProxy = new();
touchProxy.Connect(VirtualView, platformView);
renderLoopManager = new RenderLoopManager(this);
platformView.PaintSurface += OnPaintSurface;
base.ConnectHandler(platformView);
}
protected override void DisconnectHandler(SKGLView platformView)
{
paintSurfaceProxy?.Disconnect(platformView);
paintSurfaceProxy = null;
touchProxy?.Disconnect(platformView);
touchProxy = null;
renderLoopManager?.StopRenderLoop();
touchHandler?.Detach(platformView);
touchHandler = null;
platformView.PaintSurface -= OnPaintSurface;
base.DisconnectHandler(platformView);
}
@ -75,49 +75,11 @@ namespace SkiaSharp.Views.Maui.Handlers
public static void MapEnableTouchEvents(SKGLViewHandler handler, ISKGLView view)
{
handler.touchHandler ??= new SKTouchHandler(
args => view.OnTouch(args),
(x, y) => handler.OnGetScaledCoord(x, y));
handler.touchHandler?.SetEnabled(handler.PlatformView, view.EnableTouchEvents);
handler.touchProxy?.UpdateEnableTouchEvents(handler.PlatformView, view.EnableTouchEvents);
}
// helper methods
private void OnPaintSurface(object? sender, iOS.SKPaintGLSurfaceEventArgs e)
{
var newCanvasSize = e.Info.Size;
if (lastCanvasSize != newCanvasSize)
{
lastCanvasSize = newCanvasSize;
VirtualView?.OnCanvasSizeChanged(newCanvasSize);
}
if (sender is SKGLView platformView)
{
var newGRContext = platformView.GRContext;
if (lastGRContext != newGRContext)
{
lastGRContext = newGRContext;
VirtualView?.OnGRContextChanged(newGRContext);
}
}
VirtualView?.OnPaintSurface(new SKPaintGLSurfaceEventArgs(e.Surface, e.BackendRenderTarget, e.Origin, e.Info, e.RawInfo));
}
private SKPoint OnGetScaledCoord(double x, double y)
{
if (VirtualView?.IgnorePixelScaling == false && PlatformView != null)
{
var scale = PlatformView.ContentScaleFactor;
x *= scale;
y *= scale;
}
return new SKPoint((float)x, (float)y);
}
private class MauiSKGLView : SKGLView
{
public bool IgnorePixelScaling { get; set; }
@ -215,5 +177,41 @@ namespace SkiaSharp.Views.Maui.Handlers
displayLink = null;
}
}
private class PaintSurfaceProxy : SKEventProxy<ISKGLView, SKGLView>
{
private SKSizeI lastCanvasSize;
private GRContext? lastGRContext;
protected override void OnConnect(ISKGLView virtualView, SKGLView platformView) =>
platformView.PaintSurface += OnPaintSurface;
protected override void OnDisconnect(SKGLView platformView) =>
platformView.PaintSurface -= OnPaintSurface;
private void OnPaintSurface(object? sender, iOS.SKPaintGLSurfaceEventArgs e)
{
if (VirtualView is not {} view)
return;
var newCanvasSize = e.Info.Size;
if (lastCanvasSize != newCanvasSize)
{
lastCanvasSize = newCanvasSize;
view.OnCanvasSizeChanged(newCanvasSize);
}
if (sender is SKGLView platformView)
{
var newGRContext = platformView.GRContext;
if (lastGRContext != newGRContext)
{
lastGRContext = newGRContext;
view.OnGRContextChanged(newGRContext);
}
}
view.OnPaintSurface(new SKPaintGLSurfaceEventArgs(e.Surface, e.BackendRenderTarget, e.Origin, e.Info, e.RawInfo));
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
using System;
namespace SkiaSharp.Views.Maui.Platform;
internal class SKEventProxy<TVirtualView, TPlatformView>
where TVirtualView : class
where TPlatformView : class
{
private WeakReference<TVirtualView>? virtualView;
protected TVirtualView? VirtualView =>
virtualView is not null && virtualView.TryGetTarget(out var v) ? v : null;
public void Connect(TVirtualView virtualView, TPlatformView platformView)
{
this.virtualView = new(virtualView);
OnConnect(virtualView, platformView);
}
protected virtual void OnConnect(TVirtualView virtualView, TPlatformView platformView)
{
}
public void Disconnect(TPlatformView platformView)
{
virtualView = null;
OnDisconnect(platformView);
}
protected virtual void OnDisconnect(TPlatformView platformView)
{
}
}

Просмотреть файл

@ -0,0 +1,55 @@
using Microsoft.Maui;
using System;
using UIKit;
namespace SkiaSharp.Views.Maui.Platform;
internal class SKTouchHandlerProxy : SKEventProxy<IView, UIView>
{
private SKTouchHandler? touchHandler;
protected override void OnDisconnect(UIView platformView)
{
touchHandler?.Detach(platformView);
touchHandler = null;
}
public void UpdateEnableTouchEvents(UIView platformView, bool enabled)
{
if (VirtualView is null)
return;
touchHandler ??= new SKTouchHandler(
args => OnTouch(args),
(x, y) => OnGetScaledCoord(x, y));
touchHandler?.SetEnabled(platformView, enabled);
}
private void OnTouch(SKTouchEventArgs e)
{
if (VirtualView is ISKCanvasView canvasView)
canvasView.OnTouch(e);
else if (VirtualView is ISKGLView glView)
glView.OnTouch(e);
}
private SKPoint OnGetScaledCoord(double x, double y)
{
var ignore = false;
if (VirtualView is ISKCanvasView canvasView)
ignore = canvasView.IgnorePixelScaling;
else if (VirtualView is ISKGLView glView)
ignore = glView.IgnorePixelScaling;
if (ignore == false && touchHandler?.View is {} platformView)
{
var scale = platformView.ContentScaleFactor;
x *= scale;
y *= scale;
}
return new SKPoint((float)x, (float)y);
}
}

Просмотреть файл

@ -0,0 +1,100 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32515.10
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Tests.Devices", "SkiaSharp.Tests.Devices\SkiaSharp.Tests.Devices.csproj", "{1675A562-6545-4FBE-8AF7-C07784D83944}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Tests", "SkiaSharp.Tests\SkiaSharp.Tests.csproj", "{1C63B836-2628-4365-8237-08080E76117B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp", "..\binding\SkiaSharp\SkiaSharp.csproj", "{9D753C4C-D7FC-4D1B-ABF0-BF1C089B987A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HarfBuzzSharp", "..\binding\HarfBuzzSharp\HarfBuzzSharp.csproj", "{D48557C5-795D-4948-84EE-A7531DDD91DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.SceneGraph", "..\binding\SkiaSharp.SceneGraph\SkiaSharp.SceneGraph.csproj", "{8CD906F8-B3E4-48E6-8B16-EAFC0C34EAE1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Resources", "..\binding\SkiaSharp.Resources\SkiaSharp.Resources.csproj", "{AD2C6978-4F5E-E592-B565-26C357877B2C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Skottie", "..\binding\SkiaSharp.Skottie\SkiaSharp.Skottie.csproj", "{915D1D57-B059-4301-9A35-2E5EB68DED99}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.HarfBuzz", "..\source\SkiaSharp.HarfBuzz\SkiaSharp.HarfBuzz\SkiaSharp.HarfBuzz.csproj", "{6F999CA5-B67F-46A3-9A94-9E99527060F6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Views", "..\source\SkiaSharp.Views\SkiaSharp.Views\SkiaSharp.Views.csproj", "{398936B0-1B68-4F2D-B91C-6880CAC9F168}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Views.Maui.Core", "..\source\SkiaSharp.Views.Maui\SkiaSharp.Views.Maui.Core\SkiaSharp.Views.Maui.Core.csproj", "{CB3FAE69-DE1F-47FF-A158-B0EF8F5F8AF6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Views.Maui.Controls", "..\source\SkiaSharp.Views.Maui\SkiaSharp.Views.Maui.Controls\SkiaSharp.Views.Maui.Controls.csproj", "{72C22D09-AC66-4D5A-B503-D7CDA2AD6A3B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "source", "source", "{6779122B-72B0-42ED-A1E7-5029C1C0A78D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1675A562-6545-4FBE-8AF7-C07784D83944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1675A562-6545-4FBE-8AF7-C07784D83944}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1675A562-6545-4FBE-8AF7-C07784D83944}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{1675A562-6545-4FBE-8AF7-C07784D83944}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1675A562-6545-4FBE-8AF7-C07784D83944}.Release|Any CPU.Build.0 = Release|Any CPU
{1675A562-6545-4FBE-8AF7-C07784D83944}.Release|Any CPU.Deploy.0 = Release|Any CPU
{1C63B836-2628-4365-8237-08080E76117B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C63B836-2628-4365-8237-08080E76117B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C63B836-2628-4365-8237-08080E76117B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C63B836-2628-4365-8237-08080E76117B}.Release|Any CPU.Build.0 = Release|Any CPU
{9D753C4C-D7FC-4D1B-ABF0-BF1C089B987A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9D753C4C-D7FC-4D1B-ABF0-BF1C089B987A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9D753C4C-D7FC-4D1B-ABF0-BF1C089B987A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9D753C4C-D7FC-4D1B-ABF0-BF1C089B987A}.Release|Any CPU.Build.0 = Release|Any CPU
{D48557C5-795D-4948-84EE-A7531DDD91DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D48557C5-795D-4948-84EE-A7531DDD91DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D48557C5-795D-4948-84EE-A7531DDD91DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D48557C5-795D-4948-84EE-A7531DDD91DC}.Release|Any CPU.Build.0 = Release|Any CPU
{8CD906F8-B3E4-48E6-8B16-EAFC0C34EAE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CD906F8-B3E4-48E6-8B16-EAFC0C34EAE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CD906F8-B3E4-48E6-8B16-EAFC0C34EAE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CD906F8-B3E4-48E6-8B16-EAFC0C34EAE1}.Release|Any CPU.Build.0 = Release|Any CPU
{AD2C6978-4F5E-E592-B565-26C357877B2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD2C6978-4F5E-E592-B565-26C357877B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD2C6978-4F5E-E592-B565-26C357877B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD2C6978-4F5E-E592-B565-26C357877B2C}.Release|Any CPU.Build.0 = Release|Any CPU
{915D1D57-B059-4301-9A35-2E5EB68DED99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{915D1D57-B059-4301-9A35-2E5EB68DED99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{915D1D57-B059-4301-9A35-2E5EB68DED99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{915D1D57-B059-4301-9A35-2E5EB68DED99}.Release|Any CPU.Build.0 = Release|Any CPU
{6F999CA5-B67F-46A3-9A94-9E99527060F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F999CA5-B67F-46A3-9A94-9E99527060F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F999CA5-B67F-46A3-9A94-9E99527060F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F999CA5-B67F-46A3-9A94-9E99527060F6}.Release|Any CPU.Build.0 = Release|Any CPU
{398936B0-1B68-4F2D-B91C-6880CAC9F168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{398936B0-1B68-4F2D-B91C-6880CAC9F168}.Debug|Any CPU.Build.0 = Debug|Any CPU
{398936B0-1B68-4F2D-B91C-6880CAC9F168}.Release|Any CPU.ActiveCfg = Release|Any CPU
{398936B0-1B68-4F2D-B91C-6880CAC9F168}.Release|Any CPU.Build.0 = Release|Any CPU
{CB3FAE69-DE1F-47FF-A158-B0EF8F5F8AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB3FAE69-DE1F-47FF-A158-B0EF8F5F8AF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB3FAE69-DE1F-47FF-A158-B0EF8F5F8AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB3FAE69-DE1F-47FF-A158-B0EF8F5F8AF6}.Release|Any CPU.Build.0 = Release|Any CPU
{72C22D09-AC66-4D5A-B503-D7CDA2AD6A3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72C22D09-AC66-4D5A-B503-D7CDA2AD6A3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72C22D09-AC66-4D5A-B503-D7CDA2AD6A3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72C22D09-AC66-4D5A-B503-D7CDA2AD6A3B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9D753C4C-D7FC-4D1B-ABF0-BF1C089B987A} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{D48557C5-795D-4948-84EE-A7531DDD91DC} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{8CD906F8-B3E4-48E6-8B16-EAFC0C34EAE1} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{AD2C6978-4F5E-E592-B565-26C357877B2C} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{915D1D57-B059-4301-9A35-2E5EB68DED99} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{6F999CA5-B67F-46A3-9A94-9E99527060F6} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{398936B0-1B68-4F2D-B91C-6880CAC9F168} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{CB3FAE69-DE1F-47FF-A158-B0EF8F5F8AF6} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
{72C22D09-AC66-4D5A-B503-D7CDA2AD6A3B} = {6779122B-72B0-42ED-A1E7-5029C1C0A78D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {34FFBC0E-9245-423A-9A91-687C9B7FDB8B}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -3,6 +3,7 @@ using DeviceRunners.VisualRunners;
using DeviceRunners.XHarness;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Hosting;
using SkiaSharp.Views.Maui.Controls.Hosting;
namespace SkiaSharp.Tests
{
@ -23,6 +24,7 @@ namespace SkiaSharp.Tests
};
builder
.UseSkiaSharp()
.ConfigureUITesting()
.UseXHarnessTestRunner(conf => conf
.AddTestAssemblies(testAssemblies)

Просмотреть файл

@ -68,13 +68,14 @@
</ItemGroup>
<ItemGroup>
<Compile Remove="Tests\**" />
<None Include="Tests\**" />
<Compile Include="Tests\Apple\**\*.cs;Tests\iOS\**\*.cs" Condition="$(TargetFramework.Contains('-ios')) or $(TargetFramework.Contains('-maccatalyst')) or $(TargetFramework.Contains('-tvos'))" />
<Compile Include="Tests\Apple\**\*.cs;Tests\macOS\**\*.cs" Condition="$(TargetFramework.Contains('-macos'))" />
<Compile Include="Tests\Android\**\*.cs" Condition="$(TargetFramework.Contains('-android'))" />
<Compile Include="Tests\Tizen\**\*.cs" Condition="$(TargetFramework.Contains('-tizen'))" />
<Compile Include="Tests\Windows\**\*.cs" Condition="$(TargetFramework.Contains('-windows'))" />
<_PlatformCompile Include="Tests\Apple\**\*.cs;Tests\iOS\**\*.cs" Condition="$(TargetFramework.Contains('-ios')) or $(TargetFramework.Contains('-maccatalyst')) or $(TargetFramework.Contains('-tvos'))" />
<_PlatformCompile Include="Tests\Apple\**\*.cs;Tests\macOS\**\*.cs" Condition="$(TargetFramework.Contains('-macos'))" />
<_PlatformCompile Include="Tests\Android\**\*.cs" Condition="$(TargetFramework.Contains('-android'))" />
<_PlatformCompile Include="Tests\Tizen\**\*.cs" Condition="$(TargetFramework.Contains('-tizen'))" />
<_PlatformCompile Include="Tests\Windows\**\*.cs" Condition="$(TargetFramework.Contains('-windows'))" />
<_OtherCompile Include="Tests\Apple\**;Tests\iOS\**;Tests\macOS\**;Tests\Android\**;Tests\Tizen\**;Tests\Windows\**" Exclude="@(_PlatformCompile)" />
<Compile Remove="@(_OtherCompile)" />
<None Include="@(_OtherCompile)" />
</ItemGroup>
<Import Project="..\..\binding\IncludeNativeAssets.SkiaSharp.targets" />

Просмотреть файл

@ -0,0 +1,50 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Xunit;
namespace SkiaSharp.Views.Maui.Controls.Tests;
public static class MauiExtensions
{
private static readonly Rect InitialFrame = new(0, 0, -1, -1);
public static async Task WaitForLoaded(
this VisualElement element,
int timeout = 1000)
{
if (element.IsLoaded)
return;
var tcs = new TaskCompletionSource();
element.Loaded += OnLoaded;
await Task.WhenAny(tcs.Task, Task.Delay(timeout));
element.Loaded -= OnLoaded;
Assert.True(element.IsLoaded);
void OnLoaded(object? sender, EventArgs e)
{
element.Loaded -= OnLoaded;
tcs.SetResult();
}
}
public static Task WaitForLayout(
this View view,
Rect? initialFrame = default,
int timeout = 1000,
int interval = 100)
{
initialFrame ??= InitialFrame;
return AssertEx.Eventually(
() => view.Frame != initialFrame,
timeout,
interval);
}
}

Просмотреть файл

@ -0,0 +1,74 @@
using System;
using System.Threading.Tasks;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using SkiaSharp.Tests;
using Xunit;
namespace SkiaSharp.Views.Maui.Controls.Tests;
public class MemoryLeakTests : SKUITests
{
[UIFact]
public Task SKCanvasViewHandlerDoesNotLeak() =>
AssertHandlerDoesNotLeak(() =>
{
var view = new SKCanvasView();
view.PaintSurface += (sender, e) =>
{
e.Surface.Canvas.Clear(SKColors.Red);
};
view.EnableTouchEvents = true;
view.Touch += (sender, e) =>
{
view.InvalidateSurface();
};
return view;
});
[UIFact]
public Task SKGLViewHandlerDoesNotLeak() =>
AssertHandlerDoesNotLeak(() =>
{
var view = new SKGLView();
view.PaintSurface += (sender, e) =>
{
e.Surface.Canvas.Clear(SKColors.Red);
};
view.EnableTouchEvents = true;
view.Touch += (sender, e) =>
{
view.InvalidateSurface();
};
view.HasRenderLoop = true;
return view;
});
private async Task AssertHandlerDoesNotLeak(Func<View> ctor)
{
async Task<(WeakReference, WeakReference, WeakReference)> RunTest()
{
var view = ctor();
var page = new ContentPage
{
Content = view
};
await CurrentPage.Navigation.PushAsync(page);
await view.WaitForLoaded();
await view.WaitForLayout();
var viewReference = new WeakReference(view);
var handlerReference = new WeakReference(view.Handler);
var platformViewReference = new WeakReference(view.Handler.PlatformView);
await page.Navigation.PopAsync();
return (viewReference, handlerReference, platformViewReference);
}
var (viewRef, handlerRef, platformRef) = await RunTest();
await AssertEx.EventuallyGC(viewRef, handlerRef, platformRef);
}
}

Просмотреть файл

@ -0,0 +1,50 @@
#nullable enable
using System.Threading.Tasks;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using SkiaSharp.Tests;
using Xunit;
namespace SkiaSharp.Views.Maui.Controls.Tests;
[Collection("SKUITests")]
public abstract class SKUITests : SKTest, IAsyncLifetime
{
protected ContentPage CurrentPage { get; private set; } = null!;
protected IMauiContext MauiContext { get; private set; } = null!;
public async Task InitializeAsync()
{
Routing.RegisterRoute("uitests", typeof(ContentPage));
await Shell.Current.GoToAsync("uitests");
CurrentPage = (ContentPage)Shell.Current.CurrentPage;
await CurrentPage.WaitForLoaded();
MauiContext = CurrentPage.Handler!.MauiContext!;
}
public async Task DisposeAsync()
{
// pop all modals
while (Shell.Current.CurrentPage.Navigation.ModalStack.Count > 0)
{
await Shell.Current.CurrentPage.Navigation.PopModalAsync();
}
// pop until we are back at our page
while (Shell.Current.CurrentPage != CurrentPage)
{
await Shell.Current.CurrentPage.Navigation.PopAsync();
}
CurrentPage = null!;
await Shell.Current.GoToAsync("..");
Routing.UnRegisterRoute("uitests");
}
}

Просмотреть файл

@ -0,0 +1,80 @@
using System;
using System.Text;
using System.Threading.Tasks;
using Xunit.Sdk;
namespace Xunit;
public static class AssertEx
{
public static async Task Eventually(
Func<bool> assertion,
int timeout = 1000,
int interval = 100,
string message = "Assertion timed out")
{
do
{
if (assertion())
{
return;
}
await Task.Delay(interval);
timeout -= interval;
}
while (timeout >= 0);
if (!assertion())
{
throw new XunitException(message);
}
}
public static async Task EventuallyGC(params WeakReference[] references)
{
Assert.NotEmpty(references);
bool AreReferencesCollected()
{
GC.Collect();
GC.WaitForPendingFinalizers();
foreach (var reference in references)
{
Assert.NotNull(reference);
if (reference.IsAlive)
{
return false;
}
}
return true;
}
try
{
await Eventually(AreReferencesCollected);
}
catch (XunitException ex)
{
throw new XunitException(ListLivingReferences(references), ex);
}
}
private static string ListLivingReferences(WeakReference[] references)
{
var stringBuilder = new StringBuilder();
foreach (var weakReference in references)
{
if (weakReference.IsAlive && weakReference.Target is object x)
{
stringBuilder.Append($"Reference to {x} (type {x.GetType()} is still alive.\n");
}
}
return stringBuilder.ToString();
}
}