Xamarin.iOS performance
Poor application performance presents itself in many ways. It can make an application seem unresponsive, can cause slow scrolling, and can reduce battery life. However, optimizing performance involves more than just implementing efficient code. The user's experience of application performance must also be considered. For example, ensuring that operations execute without blocking the user from performing other activities can help to improve the user's experience.
This document describes techniques that can be used to improve performance and memory usage in Xamarin.iOS applications.
Note
Before reading this article you should first read Cross-Platform Performance, which discusses non-platform specific techniques to improve the memory usage and performance of applications built using the Xamarin platform.
In some situations it's possible to create strong reference cycles that could prevent objects from having their memory reclaimed by the garbage collector. For example, consider the case where an NSObject
-derived subclass, such as a class that inherits from UIView
, is added to an NSObject
-derived container and is strongly referenced from Objective-C, as shown in the following code example:
class Container : UIView
{
public void Poke ()
{
// Call this method to poke this object
}
}
class MyView : UIView
{
Container parent;
public MyView (Container parent)
{
this.parent = parent;
}
void PokeParent ()
{
parent.Poke ();
}
}
var container = new Container ();
container.AddSubview (new MyView (container));
When this code creates the Container
instance, the C# object will have a strong reference to an Objective-C object. Similarly, the MyView
instance will also have a strong reference to an Objective-C object.
In addition, the call to container.AddSubview
will increase the reference count on the unmanaged MyView
instance. When this happens, the Xamarin.iOS runtime creates a GCHandle
instance to keep the MyView
object in managed code alive, because there is no guarantee that any managed objects will keep a reference to it. From a managed code perspective, the MyView
object would be reclaimed after the AddSubview
call were it not for the GCHandle
.
The unmanaged MyView
object will have a GCHandle
pointing to the managed object, known as a strong link. The managed object will contain a reference to the Container
instance. In turn the Container
instance will have a managed reference to the MyView
object.
In circumstances where a contained object keeps a link to its container, there are several options available to deal with the circular reference:
- Manually break the cycle by setting the link to the container to
null
. - Manually remove the contained object from the container.
- Call
Dispose
on the objects. - Avoid the circular reference keeping a weak reference to the container. For more information about weak references.
One way to prevent a cycle is to use a weak reference from the child to the parent. For example, the above code could be written like this:
class Container : UIView
{
public void Poke ()
{
// Call this method to poke this object
}
}
class MyView : UIView
{
WeakReference<Container> weakParent;
public MyView (Container parent)
{
this.weakParent = new WeakReference<Container> (parent);
}
void PokeParent ()
{
if (weakParent.TryGetTarget (out var parent))
parent.Poke ();
}
}
var container = new Container ();
container.AddSubview (new MyView (container));
Here, the contained object will not keep the parent alive. However,
the parent keeps the child alive through the call done to
container.AddSubView
.
This also happens in iOS APIs that use the delegate or data
source pattern, where a peer class contains the implementation;
for example, when setting the
Delegate
property or the
DataSource
in the UITableView
class.
In the case of classes that are created purely for the sake of
implementing a protocol, for example the
IUITableViewDataSource
,
what you can do is instead of creating a subclass, you can just
implement the interface in the class and override the method, and
assign the DataSource
property to this
.
Xamarin.iOS
11.10
introduced the [Weak]
attribute. Like WeakReference <T>
,
[Weak]
can be used to break strong circular
references,
but with even less code.
Consider the following code, which uses WeakReference <T>
:
public class MyFooDelegate : FooDelegate {
WeakReference<MyViewController> controller;
public MyFooDelegate (MyViewController ctrl) => controller = new WeakReference<MyViewController> (ctrl);
public void CallDoSomething ()
{
MyViewController ctrl;
if (controller.TryGetTarget (out ctrl)) {
ctrl.DoSomething ();
}
}
}
Equivalent code using [Weak]
is far more concise:
public class MyFooDelegate : FooDelegate {
[Weak] MyViewController controller;
public MyFooDelegate (MyViewController ctrl) => controller = ctrl;
public void CallDoSomething () => controller.DoSomething ();
}
The following is another example of using [Weak]
in context of the
delegation
pattern:
public class MyViewController : UIViewController
{
WKWebView webView;
protected MyViewController (IntPtr handle) : base (handle) { }
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
webView = new WKWebView (View.Bounds, new WKWebViewConfiguration ());
webView.UIDelegate = new UIDelegate (this);
View.AddSubview (webView);
}
}
public class UIDelegate : WKUIDelegate
{
[Weak] MyViewController controller;
public UIDelegate (MyViewController ctrl) => controller = ctrl;
public override void RunJavaScriptAlertPanel (WKWebView webView, string message, WKFrameInfo frame, Action completionHandler)
{
var msg = $"Hello from: {controller.Title}";
var alertController = UIAlertController.Create (null, msg, UIAlertControllerStyle.Alert);
alertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, null));
controller.PresentViewController (alertController, true, null);
completionHandler ();
}
}
If a strong reference exists and it's difficult to remove the dependency, make a Dispose
method clear the parent pointer.
For containers, override the Dispose
method to remove the contained objects, as shown in the following code example:
class MyContainer : UIView
{
public override void Dispose ()
{
// Brute force, remove everything
foreach (var view in Subviews)
{
view.RemoveFromSuperview ();
}
base.Dispose ();
}
}
For a child object that keeps strong reference to its parent, clear the reference to the parent in the Dispose
implementation:
class MyChild : UIView
{
MyContainer container;
public MyChild (MyContainer container)
{
this.container = container;
}
public override void Dispose ()
{
container = null;
}
}
For more information about releasing strong references, see Release IDisposable Resources. There's also more garbage collection information here.
For more information, see Rules to Avoid Retain Cycles on Cocoa With Love, Is this a bug in MonoTouch GC on StackOverflow, and Why can't MonoTouch GC kill managed objects with refcount > 1? on StackOverflow.
Users expect smooth scrolling and fast load times for UITableView
instances. However, scrolling performance can suffer when cells contain deeply nested view hierarchies, or when cells contain complex layouts. However, there are techniques that can be used to avoid poor UITableView
performance:
- Reuse cells. For more information, see Reuse Cells.
- Reduce the number of subviews.
- Cache cell content that is retrieved from a web service.
- Cache the height of any rows if they aren't identical.
- Make the cell, and any other views, opaque.
- Avoid image scaling and gradients.
Collectively these techniques can help to keep UITableView
instances scrolling smoothly.
When displaying hundreds of rows in a UITableView
, it would be a waste of memory to create hundreds of UITableViewCell
objects when only a small number of them are displayed on screen at once. Instead, only the cells visible on screen can be loaded into memory, with the content being loaded into these reused cells. This prevents the instantiation of hundreds of additional objects, saving time and memory.
Therefore, when a cell disappears from the screen its view can be placed in a queue for reuse, as shown in the following code example:
class MyTableSource : UITableViewSource
{
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
// iOS will create a cell automatically if one isn't available in the reuse pool
var cell = (MyCell) tableView.DequeueReusableCell (MyCellId, indexPath);
// Perform required cell actions
return cell;
}
}
As the user scrolls, the UITableView
calls the GetCell
override to request new views to display. This override then calls the DequeueReusableCell
method and if a cell is available for reuse it will be returned.
For more information, see Cell Reuse in Populating a Table with Data.
Ensure that any views that have no transparency defined have their Opaque
property set. This will ensure that the views are optimally rendered by the drawing system. This is particularly important when a view is embedded in a UIScrollView
, or is part of a complex animation. Otherwise the drawing system will composite the views with other content, which can noticeably impact on performance.
Although XIBs have largely been replaced by storyboards, there are some circumstances where XIBs may still be used. When a XIB is loaded into memory, all of its contents are loaded into memory, including any images. If the XIB contains a view that's not immediately being used, then memory is being wasted. Therefore, when using XIBs ensure that there is only one XIB per view controller, and if possible, separate the view controller's view hierarchy into separate XIBs.
Images are some of the most expensive resources that applications use, and are often captured at high resolutions. Therefore, when displaying an image from the app's bundle in a UIImageView
, ensure that the image and UIImageView
are identically sized. Scaling images at runtime can be an expensive operation, particularly if the UIImageView
is embedded in a UIScrollView
.
For more information, see Optimize Image Resources in the Cross-Platform Performance guide.
Begin deploying and testing an application on a physical device as early as possible. Simulators do not perfectly match the behaviors and limitations of devices and so it is important to test in a real-world device scenario as early as possible.
In particular the simulator does not in any way simulate the memory or CPU restrictions of a physical device.
Games tend to have tight loops to run the game logic and update the screen. Typical frame rates range from thirty to sixty frames per second. Some developers feel that they should update the screen as many times as possible per second, combining their game simulation with updates to the screen and might be tempted to go beyond sixty frames per second.
However, the display server performs screen updates at an upper limit of sixty times per second. Therefore, attempting to update the screen faster than this limit can lead to screen tearing and micro-stuttering. It's best to structure code so that screen updates are synchronized with the display update. This can be achieved by using the CoreAnimation.CADisplayLink
class, which is a timer suitable for visualization and games that runs at sixty frames per second.
Avoiding core animation transparency improve bitmap compositing performance. In general, avoid transparent layers and blurred borders if possible.
Generating code dynamically with System.Reflection.Emit
or the Dynamic Language Runtime must be avoided because the iOS kernel prevents dynamic code execution.
This article described and discussed techniques for increasing the performance of applications built with Xamarin.iOS. Collectively these techniques can greatly reduce the amount of work being performed by a CPU, and the amount of memory consumed by an application.