DEV Community

Kamu
Kamu

Posted on

The sample of rendering faster avoiding out of memory exception in Xamarin.Forms (Android) with FFImageLoading + NativeCell

Xamarin.Forms.Android ListView is poor performance when using images in particular.
This is also the same when using Fast Renderers.

Since rendering is slow or an app crash by the memory leak when continuing to use ListView, It is necessary some workaround.

To solve it, I took measures by combining NativeCell (an implementation of each platform cell) and FFImageLoading.

That's why this article describes the sample with NativeCell + FFImageLoading for comfortably using ListView.

A finished sample project

Required library

This is the nice library that helps us with a lot of things such as caching image and load asynchronously.

In a case without taking measures

Used images as a sample are moderate to large 20 images of 500KB〜1000KB per picture.
These are positioned on ViewCell and in turn displayed 20 rows on ListView.

<ListView x:Name="listview" ItemsSource="{Binding Items}" HasUnevenRows="false" RowHeight="400">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <StackLayout Spacing="0" Margin="0,0,0,6" BackgroundColor="Silver">
                    <StackLayout.GestureRecognizers>
                        <TapGestureRecognizer Command="{Binding BindingContext.GoDetailCommand,Source={x:Reference listview}}" CommandParameter="{Binding PhotoUrl}" />
                    </StackLayout.GestureRecognizers>
                    <Image Aspect="AspectFill" Source="{Binding PhotoUrl}" HorizontalOptions="FillAndExpand" HeightRequest="300" />
                    <Label Text="{Binding Title,StringFormat='Title{0:N}'}" />
                    <Label Text="{Binding Date,StringFormat='Taken at:{0:N}'}" />
                </StackLayout>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
Enter fullscreen mode Exit fullscreen mode

Like this, the app crashes in no time.

Using FFImageLoading

FFImageLoading's CachedImage is used to avoid the problem as the previous section.

A CachedImage view is used instead of an Image view, its Downsample property is set to 320.
This works that an image is displayed not the original one but one shrunk the width to 320.

<ListView x:Name="listview" ItemsSource="{Binding Items}" HasUnevenRows="false" RowHeight="400">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <StackLayout Spacing="0" Margin="0,0,0,6" BackgroundColor="Silver">
                    <StackLayout.GestureRecognizers>
                        <TapGestureRecognizer Command="{Binding BindingContext.GoDetailCommand,Source={x:Reference listview}}" CommandParameter="{Binding PhotoUrl}" />
                    </StackLayout.GestureRecognizers>
                    <ff:CachedImage DownsampleWidth="320" Aspect="AspectFill" Source="{Binding PhotoUrl}" HorizontalOptions="FillAndExpand" HeightRequest="300" />
                    <Label Text="{Binding Title,StringFormat='Title{0:N}'}" />
                    <Label Text="{Binding Date,StringFormat='Taken at:{0:N}'}" />
                </StackLayout>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
Enter fullscreen mode Exit fullscreen mode

Here's the page loading images.

Here's the page loaded completely images.

The app came not to crash, and then displaying images came to be smooth.
More often than not, I think, the issue of out of memory is solved by this way.

FFImageLoading + NativeCell + the other workarounds for out of memory error

When the issue of out of memory occurs even if using a CachedImage, it might be solved by using a native cell.

The sample using a view cell as a native cell is here.
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/viewcell/

The way of implementing a native cell is

  1. Define MyViewCell class derived from ViewCell in a .NET Standard project.
  2. Create an XML file for layout in Resources/layout and define the layout.
  3. Define an appropriate native cell class derived from LinearLayout in an Android platform project and implement INativeElementView to it.
  4. Define MyViewCellRenderer class derived from ViewCellRenderer in an Android platform project, and attach ExportRederer Attribute.

And the other workarounds for out of memory error is

  1. Enable a largeHeap option in the AndroidManifest.xml (the most effectively).
  2. Add OnTrimMemory override method to the MainActivity class.
  3. Define MyImageViewAsync class derived from ImageViewAsync.

About more information, see the offical wiki https://github.com/luberda-molinet/FFImageLoading/wiki/Advanced-Usage#clear-cache-and-memory-considerations.

The concrete code is shown in the following sections.

Define PhotoViewCell class derived from ViewCell in a .NET Standard project.

public class PhotoViewCell : ViewCell
{
    public static BindableProperty PhotoItemProperty =
        BindableProperty.Create(
            nameof(PhotoItem),
            typeof(MainPageViewModel.PhotoItem),
            typeof(PhotoViewCell),
            null,
         defaultBindingMode: BindingMode.OneWay
        );

    public MainPageViewModel.PhotoItem PhotoItem {
        get { return (MainPageViewModel.PhotoItem)GetValue(PhotoItemProperty); }
        set { SetValue(PhotoItemProperty, value); }
    }

    public static BindableProperty CommandProperty =
        BindableProperty.Create(
            nameof(Command),
            typeof(ICommand),
            typeof(PhotoViewCell),
            null,
         defaultBindingMode: BindingMode.OneWay
        );

    public ICommand Command {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }
}
Enter fullscreen mode Exit fullscreen mode

This is what communicates with the platform project.
Though individual title and URL could be set, passing them is troublesome, so I decided to pass a unit of the following class.

public class PhotoItem
{
    public string PhotoUrl { get; set; }
    public string Title { get; set; }
    public string Date { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Create an XML file for layout in Resources/layout and define the layout.

The layout is defined with XML. In turn, this is named "PhotoViewCell.axml" and saved in Resources/Layout.
In this sample, a custom ImageView is used for more efficient memory usage. But you can use any ImageView.

The custom ImageView are discussed in more detail in the section Define MyImageViewAsync class derived from ImageViewAsync below.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="406dp"
    android:minHeight="406dp">
    <sample.droid.cells.MyImageView
        android:id="@+id/PhotoViewImage"
        android:scaleType="centerCrop"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:minHeight="300dp" />
    <TextView
        android:id="@+id/PhotoViewTitle"
        android:layout_below="@id/PhotoViewImage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/PhotoViewDate"
        android:layout_below="@id/PhotoViewTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <LinearLayout
        android:background="#FFFFFF"
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:layout_height="6dp" />
</RelativeLayout>
Enter fullscreen mode Exit fullscreen mode

Define native cell class

This cell is what is displayed in native ListView actually.
In the constructor, the corresponding layout is inflated, the reference to each control is getting, and the OnClickListener is added.

In the UpdateCell method, the current cell contents are updated after the previous task is cleared in order to be appropriately updated.
In concrete, if the previous loading task is running, it is canceled, which in turn the current task is executed.
Without this process, it would be poor performance because the unnecessary vain processes go on running.

In the CellPropertyChanged method, UpdateCell method is called when the forms cell is updated if Xamarin.Forms.Listview cache strategy is RecycleElement.
It is also called when changing dynamically Xamarin.Forms cell value.
This is not needed if cache strategy is RetainElement.

INativeElementView is what RecycleElement needs, in which case it must be implemented.

public class PhotoNativeCell : LinearLayout, INativeElementView, Android.Views.View.IOnClickListener
{
    public PhotoViewCell PhotoViewCell { get; set; }
    public Element Element => PhotoViewCell;
    public IScheduledWork CurrentTask { get; set; }

    public MyImageView ImageView { get; set; }
    public TextView Title { get; set; }
    public TextView Date { get; set; }

    public PhotoNativeCell(Context context, PhotoViewCell formsCell) : base(context)
    {
        var view = (context as FormsAppCompatActivity).LayoutInflater.Inflate(Resource.Layout.PhotoViewCell, this, true);

        PhotoViewCell = formsCell;

        ImageView = view.FindViewById<MyImageView>(Resource.Id.PhotoViewImage);
        Title = view.FindViewById<TextView>(Resource.Id.PhotoViewTitle);
        Date = view.FindViewById<TextView>(Resource.Id.PhotoViewDate);

        SetOnClickListener(this);
    }

    public void CellPropertyChanged(object sender,PropertyChangedEventArgs e)
    {
        if (e.PropertyName == PhotoViewCell.PhotoItemProperty.PropertyName) {
            // Update for when the cache strategy of Xamarin.Forms.ListView is RecycleElement
            // or changing dynamically Xamarin.Forms cell value.
            UpdateCell();
        }
    }

    public void UpdateCell()
    {

        // If the previous task is no completed and no canceled, the task is canceled. 
        if (CurrentTask != null && !CurrentTask.IsCancelled && !CurrentTask.IsCompleted) {
            CurrentTask.Cancel();
        }

        // An alternate image until the image is completely loaded (here is transparent).
        ImageView.SetImageResource(global::Android.Resource.Color.Transparent);

        // Begin loading the image
        CurrentTask = ImageService.Instance.LoadUrl(PhotoViewCell.PhotoItem.PhotoUrl).DownSample(320).Into(ImageView);
        // Set the key in order to clear the CachedImage memory cache when finalizing.
        ImageView.Key = PhotoViewCell.PhotoItem.PhotoUrl;

        // Update the cell's text
        Title.Text = PhotoViewCell.PhotoItem.Title;
        Date.Text = PhotoViewCell.PhotoItem.Date;
    }

    public void OnClick(Android.Views.View v)
    {
        PhotoViewCell.Command?.Execute(PhotoViewCell.PhotoItem.PhotoUrl);
    }
}
Enter fullscreen mode Exit fullscreen mode

Define CustomCellRenderer

This class is what associate a forms cell with a native cell.

In GetCellCore method, the item is assigned the forms cell and convertView is assigned null or the native cell.

What the convertView is null means that it is a new cell; otherwise means that it is a recycle cell.
So the former have to create a new native cell, the latter have to clean up the previous cell.

In both cases, Updating the referred forms cell and the PropertyChanged event is needed.

The concrete process is done by the side of a native cell. About more information, see the above section Define native cell class.

[assembly: ExportRenderer(typeof(PhotoViewCell), typeof(PhotoViewCellRenderer))]
namespace Sample.Droid.Cells
{
    public class PhotoViewCellRenderer : ViewCellRenderer
    {
        protected override Android.Views.View GetCellCore(Xamarin.Forms.Cell item, Android.Views.View convertView, Android.Views.ViewGroup parent, Android.Content.Context context)
        {
            var formsCell = item as PhotoViewCell;
            var nativeCell = convertView as PhotoNativeCell;

            if (nativeCell == null) {
                // Creating a new real native cell. 
                nativeCell = new PhotoNativeCell(context, formsCell);
            }

            // Unsubscribe the privious formscell propertychanged event on the nativecell.
            nativeCell.PhotoViewCell.PropertyChanged -= nativeCell.CellPropertyChanged;

            // Update the formscell reffered by the nativecell. 
            nativeCell.PhotoViewCell = formsCell;

            // Subscribe the current formscell propertychanged event on the nativecell.
            nativeCell.PhotoViewCell.PropertyChanged += nativeCell.CellPropertyChanged;

            // Update the nativecell contents with the current formscell contents.
            nativeCell.UpdateCell();

            return nativeCell;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Xaml for the native cell (Android)

In this sample case, as the native cell is used on only Android, the Xaml is divided into the native cell and ViewCell using OnPlatform tag.

The below is how the native cell is used only on Android.

<ListView x:Name="listview" CachingStrategy="RecycleElement" ItemsSource="{Binding Items}" HasUnevenRows="false" RowHeight="400">
    <ListView.ItemTemplate>
        <DataTemplate>
            <cell:PhotoViewCell PhotoItem="{Binding}" Command="{Binding BindingContext.GoDetailCommand,Source={x:Reference listview}}">
                <!-- Above properties are just available on Android -->
                <!-- Below ViewCell content is just available on iOS -->
                <ViewCell.View>
                    <OnPlatform x:TypeArguments="View">
                        <On Platform="iOS">
                            <StackLayout Spacing="0" Margin="0,0,0,6" BackgroundColor="Silver">
                                <StackLayout.GestureRecognizers>
                                    <TapGestureRecognizer Command="{Binding BindingContext.GoDetailCommand,Source={x:Reference listview}}" CommandParameter="{Binding PhotoUrl}" />
                                </StackLayout.GestureRecognizers>
                                <ff:CachedImage DownsampleWidth="320" Aspect="AspectFill" Source="{Binding PhotoUrl}" HorizontalOptions="FillAndExpand" HeightRequest="300" />
                                <Label Text="{Binding Title,StringFormat='Title:{0:N}'}" />
                                <Label Text="{Binding Date,StringFormat='Taken at:{0:N}'}" />
                            </StackLayout>
                        </On>
                    </OnPlatform>
                </ViewCell.View>
            </cell:PhotoViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
Enter fullscreen mode Exit fullscreen mode

This speed is, as far as I see, as good as ViewCell + FFImageLoading.
But I think that memory will be used efficiently.

Enable a largeHeap option in the AndroidManifest.xml

If the memory size used an app is wanted to be large, you write android:largeHeap="true" in the AndroidManifest.xml as the following.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="kamusoft.sample">
    <uses-sdk android:minSdkVersion="21" />
    <application android:label="Sample" android:largeHeap="true"></application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

Though this has nothing to do with memory efficient, I think that it is the most effective for out of memory error.

Add OnTrimMemory override method to the MainActivity class

For more information, see https://github.com/luberda-molinet/FFImageLoading/wiki/Advanced-Usage#clear-cache-and-memory-considerations

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    ...
    public override void OnTrimMemory([GeneratedEnum] TrimMemory level)
    {
        ImageService.Instance.InvalidateMemoryCache();
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        base.OnTrimMemory(level);
    }
}
Enter fullscreen mode Exit fullscreen mode

Define MyImageViewAsync class derived from ImageViewAsync

This custom ImageViewAsync clears memory cache when JavaFinalize.
Register attribute is what enables the custom view in XML.

For more information, see https://github.com/luberda-molinet/FFImageLoading/wiki/Advanced-Usage#clear-cache-and-memory-considerations

[Android.Runtime.Preserve(AllMembers = true)]
[Register("sample.droid.cells.MyImageView")]
public class MyImageView : ImageViewAsync
{
    public string Key { get; set; }

    public MyImageView(Context context) : base(context)
    {
    }

    public MyImageView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
    {
    }

    public MyImageView(Context context, IAttributeSet attrs) : base(context, attrs)
    {
    }

    protected override void JavaFinalize()
    {
        SetImageDrawable(null);
        SetImageBitmap(null);
        ImageService.Instance.InvalidateCacheEntryAsync(Key, FFImageLoading.Cache.CacheType.Memory);
        base.JavaFinalize();
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This article introduced several workarounds with FFImageLoading for out of memory.

More often than not, it solves if only replacing a default Image view with a CachedImage.

Nevertheless, if out of memory error occurs, as this latter section discussed, you can fit together such as a native cell and lerge heap memory setting.

In that case, there is demerit that you have to create more classes.
But the more complex the design has, the more the merit is when using a native cell.
So I think that it is better to judge by what you want to give priority to.

Thank you for reading the too long article.

About pictures used by this sample app.

http://sozai-free.com/
Thanks.

Top comments (0)