Skip to content

Commit

Permalink
[iOS] Improve background layer frame mapping performance (#24848)
Browse files Browse the repository at this point in the history
* [iOS] Improve background layer frame mapping performance

* Use a low-level observer object to avoid random crash

* Add UITest

* Add device test to bring proof of not leaking memory

* IAutoSizableCALayer as internal interface

* Add UI test for #26057

* Revert compatibility changes

---------

Co-authored-by: Rui Marinho <[email protected]>
Co-authored-by: Tamilarasan Paranthaman <[email protected]>
  • Loading branch information
3 people authored Dec 10, 2024
1 parent ba3aec2 commit ffb0db2
Show file tree
Hide file tree
Showing 25 changed files with 385 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ public override void LayoutSubviews()
if (_previousSize != Bounds.Size)
{
SetNeedsDisplay();
this.UpdateBackgroundLayer();
}

base.LayoutSubviews();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ public static void InsertBackgroundLayer(this CALayer layer, CALayer backgroundL
layer.InsertSublayer(backgroundLayer, index);
else
layer.AddSublayer(backgroundLayer);

(backgroundLayer as IAutoSizableCALayer)?.AutoSizeToSuperLayer();
}
}

Expand Down Expand Up @@ -176,6 +178,7 @@ public static void RemoveBackgroundLayer(this CALayer layer)
}
}

[Obsolete("MAUI background layers now automatically update their Frame when their SuperLayer Frame changes. This method will be removed in a future release.")]
public static void UpdateBackgroundLayer(this UIView view) =>
view.UpdateBackgroundLayerFrame(BackgroundLayer);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue24847.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 24847, "[iOS] Background layer frame mapping has poor performance", PlatformAffected.iOS)]
public class Issue24847 : ContentPage
{
readonly Border _borderView;
readonly Frame _frameView;

public Issue24847()
{
var linearGradient = new LinearGradientBrush(
[
new GradientStop(Colors.Aqua, 0),
new GradientStop(Colors.SlateBlue, 1)
],
new Point(0, 0),
new Point(1, 1)
);

_borderView = new Border { Background = linearGradient };
_frameView = new Frame { Background = linearGradient };

var button = new Button
{
Text = "Change size",
AutomationId = "ChangeSizeBtn",
};
button.Clicked += (s, e) =>
{
ToggleSize();
};

var vsl = new VerticalStackLayout
{
Spacing = 16,
Padding = 16
};

vsl.Add(_borderView);
vsl.Add(_frameView);
vsl.Add(button);

ToggleSize();

Content = vsl;
}

void ToggleSize()
{
var targetSize = _borderView.WidthRequest == 100 ? 200 : 100;

_borderView.WidthRequest = targetSize;
_borderView.HeightRequest = targetSize;
_frameView.WidthRequest = targetSize;
_frameView.HeightRequest = targetSize;
}
}
28 changes: 28 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue26057.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue26057">

<StackLayout x:Name="stack">
<Button Text ="Remeasure the StackLayout" AutomationId="Button" Clicked="OnButtonClicked"/>
<ContentView>
<Button x:Name="button"
WidthRequest="100"
HeightRequest="100"
Text="Hello">
<Button.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="white"
Offset="0"/>
<GradientStop Color="Red"
Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Button.Background>
</Button>
</ContentView>
</StackLayout>
</ContentPage>
20 changes: 20 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue26057.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using Microsoft.Maui.Controls.Internals;

namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.Github, 26057, "[iOS & Mac] Gradient background size is incorrect when invalidating parent", PlatformAffected.iOS | PlatformAffected.macOS)]
public partial class Issue26057: ContentPage
{
public Issue26057()
{
InitializeComponent();
}

private void OnButtonClicked(object sender, EventArgs e)
{
(stack as IView).InvalidateMeasure();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#if IOS

using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue24847 : _IssuesUITest
{
public Issue24847(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "[iOS] Background layer frame mapping has poor performance";

[Test]
[Category(UITestCategories.Visual)]
public void BackgroundFrameResizesFastAndCorrectly()
{
App.WaitForElement("ChangeSizeBtn");
VerifyScreenshot("BackgroundFrameResizesFastAndCorrectly");

App.Tap("ChangeSizeBtn");
VerifyScreenshot("BackgroundFrameResizesFastAndCorrectlySizeChanged");
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue26057 : _IssuesUITest
{
public Issue26057(TestDevice device) : base(device) { }

public override string Issue => "[iOS & Mac] Gradient background size is incorrect when invalidating parent";

[Test]
[Category(UITestCategories.Button)]
public void GradientLayerShouldApplyProperly()
{
App.WaitForElement("Button");
App.Tap("Button");
VerifyScreenshot();
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions src/Core/src/Handlers/View/ViewHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ internal static void MapContextFlyout(IElementHandler handler, IContextFlyoutEle
static partial void MappingFrame(IViewHandler handler, IView view)
{
UpdateTransformation(handler, view);
if (handler.VirtualView?.Background is not null)
handler.ToPlatform().UpdateBackgroundLayerFrame();
}

public static void MapTranslationX(IViewHandler handler, IView view)
Expand Down
56 changes: 56 additions & 0 deletions src/Core/src/Platform/iOS/CALayerAutosizeObserver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using CoreAnimation;
using CoreGraphics;
using Foundation;

namespace Microsoft.Maui.Platform;

[Register("MauiCALayerAutosizeObserver")]
class CALayerAutosizeObserver : NSObject
{
static readonly NSString _boundsKey = new("bounds");

readonly WeakReference<CALayer> _layerReference;
bool _disposed;

public static CALayerAutosizeObserver Attach(CALayer layer)
{
_ = layer ?? throw new ArgumentNullException(nameof(layer));

var superLayer = layer.SuperLayer ?? throw new InvalidOperationException("SuperLayer should be set before creating CALayerAutosizeObserver");
var observer = new CALayerAutosizeObserver(layer);
superLayer.AddObserver(observer, _boundsKey, NSKeyValueObservingOptions.New, observer.Handle);
layer.Frame = superLayer.Bounds;
return observer;
}

private CALayerAutosizeObserver(CALayer layer)
{
_layerReference = new WeakReference<CALayer>(layer);
IsDirectBinding = false;
}

[Preserve (Conditional = true)]
public override void ObserveValue (NSString keyPath, NSObject ofObject, NSDictionary change, IntPtr context)
{
if (!_disposed && keyPath == _boundsKey && context == Handle && _layerReference.TryGetTarget(out var layer))
{
layer.Frame = layer.SuperLayer?.Bounds ?? CGRect.Empty;
}
}

protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;

if (_layerReference.TryGetTarget(out var layer))
{
layer?.SuperLayer?.RemoveObserver(this, _boundsKey);
}
}

base.Dispose(disposing);
}
}
1 change: 0 additions & 1 deletion src/Core/src/Platform/iOS/ContentView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public override void LayoutSubviews()
base.LayoutSubviews();

UpdateClip();
this.UpdateBackgroundLayerFrame();
}

internal IBorderStroke? Clip
Expand Down
15 changes: 15 additions & 0 deletions src/Core/src/Platform/iOS/IAutoSizableCALayer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Microsoft.Maui.Platform
{
/// <summary>
/// Provides a way to automatically size a CALayer to its super layer.
/// </summary>
// TODO: When we're good with this solution, we should make this public with NET10.
// We may also evaluate to make CALayerAutosizeObserver public to offer a solution for third-party developers.
internal interface IAutoSizableCALayer
{
/// <summary>
/// Automatically sizes the CALayer to its super layer.
/// </summary>
void AutoSizeToSuperLayer();
}
}
2 changes: 2 additions & 0 deletions src/Core/src/Platform/iOS/LayerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public static void InsertBackgroundLayer(this UIView control, CALayer background
layer.InsertSublayer(backgroundLayer, index);
else
layer.AddSublayer(backgroundLayer);

(backgroundLayer as IAutoSizableCALayer)?.AutoSizeToSuperLayer();
}
}

Expand Down
26 changes: 24 additions & 2 deletions src/Core/src/Platform/iOS/MauiCALayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Microsoft.Maui.Platform
{
public class MauiCALayer : CALayer
public class MauiCALayer : CALayer, IAutoSizableCALayer
{
CGRect _bounds;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "IShape is a non-NSObject in MAUI.")]
Expand All @@ -30,13 +30,35 @@ public class MauiCALayer : CALayer

nfloat _strokeMiterLimit;

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in CALayerAutosizeObserver_DoesNotLeak test.")]
CALayerAutosizeObserver? _boundsObserver;

public MauiCALayer()
{
_bounds = new CGRect();

ContentsScale = UIScreen.MainScreen.Scale;
}

protected override void Dispose(bool disposing)
{
_boundsObserver?.Dispose();
_boundsObserver = null;
base.Dispose(disposing);
}

public override void RemoveFromSuperLayer()
{
_boundsObserver?.Dispose();
_boundsObserver = null;
base.RemoveFromSuperLayer();
}

void IAutoSizableCALayer.AutoSizeToSuperLayer()
{
_boundsObserver?.Dispose();
_boundsObserver = CALayerAutosizeObserver.Attach(this);
}

public override void AddAnimation(CAAnimation animation, string? key)
{
// Do nothing, we don't want animations here
Expand Down
26 changes: 25 additions & 1 deletion src/Core/src/Platform/iOS/StaticCAGradientLayer.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using CoreAnimation;

namespace Microsoft.Maui.Platform;

class StaticCAGradientLayer : CAGradientLayer
class StaticCAGradientLayer : CAGradientLayer, IAutoSizableCALayer
{
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in CALayerAutosizeObserver_DoesNotLeak test.")]
CALayerAutosizeObserver? _boundsObserver;

protected override void Dispose(bool disposing)
{
_boundsObserver?.Dispose();
_boundsObserver = null;
base.Dispose(disposing);
}

public override void RemoveFromSuperLayer()
{
_boundsObserver?.Dispose();
_boundsObserver = null;
base.RemoveFromSuperLayer();
}

void IAutoSizableCALayer.AutoSizeToSuperLayer()
{
_boundsObserver?.Dispose();
_boundsObserver = CALayerAutosizeObserver.Attach(this);
}

public override void AddAnimation(CAAnimation animation, string? key)
{
// Do nothing, we don't want animations here
Expand Down
Loading

0 comments on commit ffb0db2

Please sign in to comment.