Xamarin.Android Performance
There are many techniques for increasing the performance of applications built with Xamarin.Android. Collectively these techniques can greatly reduce the amount of work being performed by a CPU, and the amount of memory consumed by an application. This article describes and discusses these techniques.
Performance Overview
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.
There are a number of techniques for increasing the performance, and perceived performance, of applications built with Xamarin.Android. They include:
- Optimize Layout Hierarchies
- Optimize List Views
- Remove Event Handlers in Activities
- Limit the Lifespan of Services
- Release Resources when Notified
- Release Resources when the User Interface is Hidden
- Optimize Image Resources
- Dispose of Unused Image Resources
- Avoid Floating-Point Arithmetic
- Dismiss Dialogs
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.
Optimize Layout Hierarchies
Each layout added to an application requires initialization, layout, and drawing. The layout pass can be expensive when nesting LinearLayout
instances that use the weight
parameter, because each child will be measured twice. Using nested instances of LinearLayout
can lead to a deep view hierarchy, which can result in poor performance for layouts that are inflated multiple times, such as in a ListView
. Therefore, it's important that such layouts are optimized, as the performance benefits will then be multiplied.
For example, consider the LinearLayout
for a list view row that has an icon, a title, and a description. The LinearLayout
will contain an ImageView
and a vertical LinearLayout
that contains two TextView
instances:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="5dip">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginRight="5dip"
android:src="@drawable/icon" />
<LinearLayout
android:orientation="vertical"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="fill_parent">
<TextView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Mei tempor iuvaret ad." />
<TextView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:singleLine="true"
android:ellipsize="marquee"
android:text="Lorem ipsum dolor sit amet." />
</LinearLayout>
</LinearLayout>
This layout is 3-levels deep, and is wasteful when inflated for each ListView
row. However, it can be improved by flattening the layout, as shown in the following code example:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="5dip">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_marginRight="5dip"
android:src="@drawable/icon" />
<TextView
android:id="@+id/secondLine"
android:layout_width="fill_parent"
android:layout_height="25dip"
android:layout_toRightOf="@id/icon"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:singleLine="true"
android:ellipsize="marquee"
android:text="Lorem ipsum dolor sit amet." />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/icon"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_above="@id/secondLine"
android:layout_alignWithParentIfMissing="true"
android:gravity="center_vertical"
android:text="Mei tempor iuvaret ad." />
</RelativeLayout>
The previous 3-level hierarchy has been reduced to a 2-level hierarchy, and a single RelativeLayout
has replaced two LinearLayout
instances. A significant performance increase will be gained when inflating the layout for each ListView
row.
Optimize List Views
Users expect smooth scrolling and fast load times for ListView
instances. However, scrolling performance can suffer when each list view row contains deeply nested view hierarchies, or when list view rows contain complex layouts. However, there are techniques that can be used to avoid poor ListView
performance:
- Reuse row views For more information, see Reuse Row Views.
- Flatten layouts, where possible.
- Cache row content that is retrieved from a web service.
- Avoid image scaling.
Collectively these techniques can help to keep ListView
instances scrolling smoothly.
Reuse Row Views
When displaying hundreds of rows in a ListView
, it would be a waste of memory to create hundreds of View
objects when only a small number of them are displayed on screen at once. Instead, only the View
objects visible in the rows on screen can be loaded into memory, with the content being loaded into these reused objects. This prevents the instantiation of hundreds of additional objects, saving time and memory.
Therefore, when a row disappears from the screen its view can be placed in a queue for reuse, as shown in the following code example:
public override View GetView(int position, View convertView, ViewGroup parent)
{
View view = convertView; // re-use an existing view, if one is supplied
if (view == null) // otherwise create a new one
view = context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem1, null);
// set view properties to reflect data for the given row
view.FindViewById<TextView>(Android.Resource.Id.Text1).Text = items[position];
// return the view, populated with data, for display
return view;
}
As the user scrolls, the ListView
calls the GetView
override to request new views to display – if available it passes an unused view in the convertView
parameter. If this value is null
then the code creates a new View
instance, otherwise the convertView
properties can be reset and reused.
For more information, see Row View Re-Use in Populating a ListView with Data.
Remove Event Handlers in Activities
When an activity is destroyed in the Android runtime, it could still be alive in the Mono runtime. Therefore, remove event handlers to external objects in Activity.OnPause
to prevent the runtime from keeping a reference to an activity that has been destroyed.
In an activity, declare event handler(s) at class level:
EventHandler<UpdatingEventArgs> service1UpdateHandler;
Then implement the handlers in the activity, such as in OnResume
:
service1UpdateHandler = (object s, UpdatingEventArgs args) => {
this.RunOnUiThread (() => {
this.updateStatusText1.Text = args.Message;
});
};
App.Current.Service1.Updated += service1UpdateHandler;
When the activity exits the running state, OnPause
is called. In the OnPause
implementation, remove the handlers as follows:
App.Current.Service1.Updated -= service1UpdateHandler;
Limit the Lifespan of Services
When a service starts, Android keeps the service process running. This makes the process expensive because its memory can't be paged, or used elsewhere. Leaving a service running when it's not required therefore increases the risk of an application exhibiting poor performance due to memory constraints. It can also make application switching less efficient as it reduces the number of processes Android can cache.
The lifespan of a service can be limited by using an IntentService
, which terminates itself once it's handled the intent that started it.
Release Resources when Notified
During the application lifecycle, the OnTrimMemory
callback provides a notification when the device memory is low. This callback should be implemented to listen for the following memory level notifications:
TrimMemoryRunningModerate
– the application may want to release some unneeded resources.TrimMemoryRunningLow
– the application should release unneeded resources.TrimMemoryRunningCritical
– the application should release as many non-critical processes as it can.
In addition, when the application process is cached, the following memory level notifications may be received by the OnTrimMemory
callback:
TrimMemoryBackground
– release resources that can be quickly and efficiently rebuilt if the user returns to the app.TrimMemoryModerate
– releasing resources can help the system keep other processes cached for better overall performance.TrimMemoryComplete
– the application process will soon be terminated if more memory isn't soon recovered.
Notifications should be responded to by releasing resources based on the received level.
Release Resources when the User Interface is Hidden
Release any resources used by the app's user interface when the user navigates to another app, as it can significantly increase Android's capacity for cached processes, which in turn can have an impact on the user experience quality.
To receive a notification when the user exits the UI, implement the OnTrimMemory
callback in Activity
classes and listen for the TrimMemoryUiHidden
level, which indicates that the UI is hidden from view. This notification will be received only when all the UI components of the application become hidden from the user. Releasing UI resources when this notification is received ensures that if the user navigates back from another activity in the app, the UI resources are still available to quickly resume the activity.
Optimize Image Resources
Images are some of the most expensive resources that applications use, and are often captured at high resolutions. Therefore, when displaying an image, display it at the resolution required for the device's screen. If the image is of a higher resolution than the screen, it should be scaled down.
For more information, see Optimize Image Resources in the Cross-Platform Performance guide.
Dispose of Unused Image Resources
To save on memory usage, it is a good idea to dispose of large image
resources that are no longer needed. However, it is important to ensure
that images are disposed of correctly. Instead of using an explicit
.Dispose()
invocation, you can take advantage of
using
statements to ensure correct use of IDisposable
objects.
For example, the
Bitmap class implements
IDisposable
. Wrapping the instantiation of a BitMap
object in a
using
block ensures that it will be disposed of correctly
on exit from the block:
using (Bitmap smallPic = BitmapFactory.DecodeByteArray(smallImageByte, 0, smallImageByte.Length))
{
// Use the smallPic bit map here
}
For more information about releasing disposable resources, see Release IDisposable Resources.
Avoid Floating-Point Arithmetic
On Android devices, floating-point arithmetic is about 2x slower than integer arithmetic. Therefore, replace floating-point arithmetic with integer arithmetic if possible. However, there's no execution time difference between float
and double
arithmetic on recent hardware.
Note
Even for integer arithmetic, some CPUs lack hardware divide capabilities. Therefore, integer division and modulus operations are often performed in software.
Dismiss Dialogs
When using the ProgressDialog
class (or any dialog or alert), instead of calling the Hide
method when the dialog's purpose is complete, call the Dismiss
method. Otherwise, the dialog will still be alive and will leak the activity by holding a reference to it.
Summary
This article described and discussed techniques for increasing the performance of applications built with Xamarin.Android. Collectively these techniques can greatly reduce the amount of work being performed by a CPU, and the amount of memory consumed by an application.