📜 ⬆️ ⬇️

Optimize memory usage in Xamarin applications

This is a translation of the article by Samuel Debruyn . I liked the article so much that a spontaneous desire appeared to share with the habr community :)

Xamarin is amazing because it allows .NET developers to write applications for Android, iOS, macOS on ... C #. But this amazing opportunity has its price, and even the simplest application can easily consume an indecent amount of memory. Let's see how this happens and what we can do with it. Most of my examples are based on Xamarin.Android, but you will quickly notice that this also applies to Xamarin.iOS.


How does garbage collector work in Xamarin applications


In fact, Xamarin applications use several types of objects. Each Xamarin application has objects that live in two separate worlds:



From this it also follows that 2 garbage collectors exist and operate:



Let's first consider SGEN. In fact, Xamarin University has some very interesting lectures on this topic, and official documentation explains this very well.


I will not go into the details of how SGEN works. Leave this topic for my next post. All we need to know now is that we can try to call the full garbage collection with the GC.Collect() command, as well as garbage collection for zero-generation (freshest) CG.Collect(0) with the CG.Collect(0) command. Most of the remaining commands are not implemented in Mono at the time of this writing. Alternatively, you can use the snapshot function in the Xamarin Profiler to speed up garbage collection.


Completing garbage collection with SGEN also starts garbage collection in a different, native world.


Peer objects


Did I mention two types of objects in Xamarin? Yes and no. All our objects live in each of two worlds, but in fact we use the third type of objects:



Next, we can divide the peer objects into two categories:



So, as a Xamarin developer, you have the right to create managed objects or user peers.


A few examples:



What is the difference between them? Let's take a look at this from the Android side (similarly for iOS).


Framework peer is often called Managed Callable Wrapper (MCW). This name tells us that:



If you were engaged in the creation of Android binding projects in Xamarin / Visual Studio, then know that you created MCW. Under the hood, Xamarin generates code that calls native methods from the Android world. To achieve this, they use the JNI (Java Native Interface). If you want to call a method that exists in the Android world, but for which you have not yet done a wrapper in Xamarin, you can use JNI to call this method.


User peer is often called Android Callable Wrapper (ACW). In turn, this name tells us that:



So, in fact, we can say that each peer object actually consists of two objects living in memory: a real (native or Mono) and a wrapper object.


This structure allows Xamarin to work on completely different platforms and this is why Xamarin is so cool. All this allows Xamarin developers to write applications quite simply, but the lack of understanding of how this works is often a source of memory problems in Xamarin applications.


Attention! Classic Bitmap Example


The most common "big" objects in Xamarin applications are bitmaps (pictures). Almost every application contains at least a few pictures in order to look more attractive. But this one has its price, these pictures are most often the largest objects in the memory of your application.


However, if you allow the android to load a bitmap and see how much it weighs in memory in any way convenient for you, you will most likely notice that the size will be insignificant. Even a 5 MB picture will occupy several bytes.


How is this possible? Where are the 5 MB? For the world of Mono, this picture is nothing more than a wrapper for a native object. This native object takes 5 MB in memory.


Well, let's say, but how can this be a source of any problems and how does this generally relate to the topic of a post? Let's take a look at the Activity code below:


 [Activity(Label = "App1", MainLauncher = true, Icon = "@drawable/icon")] public class MainActivity : Activity { protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); SetContentView(Resource.Layout.Main); for (int i = 0; i < 100; i++) { var hugeBitmap = Android.Graphics.BitmapFactory.DecodeFile($"path/to/bitmaps/{i}.png"); if(!ImageContainsUnicorn(hugeBitmap)) { continue; } var imageView = FindViewById<ImageView>(Resource.Id.SomeImageView); imageView.SetImageBitmap(hugeBitmap); } } } 

This code loads 100 bitmaps and checks whether the image contains a unicorn, if so, it displays it in an ImageView. We use only one bitmap at the end, so there shouldn't be any memory problems, because as soon as the selected bitmaps go out of sight, they will be collected by the garbage collector, right?


Wrong. The application will OutOfMemoryException in a few milliseconds due to an OutOfMemoryException . In order to understand why this is happening, let's see how Xamarin works in this situation.


The hugeBitmap variable is MCW, and the size of this object in Mono will be negligible. The code above should not normally run garbage collection (in the Mono world).


On the other hand, the android will go crazy, and the garbage collector will work at a crazy pace. However, he will not be able to find the objects to be assembled. The garbage collector cannot collect bitmaps, because they will still refer to wrapper objects in the controlled (Mono) world. Until the managed wrapper objects are collected by SGEN, the native garbage collector cannot do anything. As a result, in the native world, your application will catch an OutOfMemoryException .


What we can do?


Each peer object implements an IDisposable interface. Let's quickly see how this is implemented:



I note that the implementation above for Xamarin.Android is no longer used in the latest version because they switched to using Java.Interop. Although the implementation of this in itself is completely different, the way of working is very similar to the old way.


As we see, the call to Dispose() breaks the bridge between the wrapper object and the object being wrapped (native). This removes the links and after disposing of the wrapper object, the native object can be collected by the garbage collector, of course, if this object does not have any references in the native world.


Wonderful! So I just need to always call Dispose() on all objects?

Almost, but not quite. In fact, we can improve the code above using the using construct. As we know, using immediately calls Dispose() after the end of the using block. In 99% of cases, it is committed to properly dispose of the framework peers immediately after calling the method / property you need. A native object will continue to live as long as it is needed and you do not break anything except a link to this object.


An enhanced version of the code above would look like this:


 protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); SetContentView(Resource.Layout.Main); for (int i = 0; i < 1000; i++) { using(var hugeBitmap = Android.Graphics.BitmapFactory.DecodeFile($"path/to/bitmaps/{i}.png")) { if (!ImageContainsUnicorn(hugeBitmap)) { continue; } using(var imageView = FindViewById<ImageView>(Resource.Id.SomeImageView)) { imageView.SetImageBitmap(hugeBitmap); } } } } 

However, if you need to use the ImageView in another method, for example, in OnResume() , the best place to dispose of the ImageView might be OnDestroy() or Dispose() itself. You can argue and say that you can simply call FindViewById() as many times as necessary, but this is a very expensive operation and should be avoided. I usually use this method at the very end of the object's life cycle, or I override the Dispose() method. This is not something you have to do, but it would certainly help reduce memory usage in your application.


A brief note about the events


You probably already guessed that everything described above applies to events. Never forget to unsubscribe from your events in the last method of the life cycle of an activity, view controller, etc. or SGEN will never collect your object. If your object has a link to peer objects, then these peer objects will live forever.


Why you should avoid calling Dispose () on user peer objects


When the time comes, be sure Xamarin will call Dispose() for any user peer object. But for us, application developers, it is not so easy to understand when this time should come. In general, the documentation tells us that you should never call Dispose() manually for user peer objects. Just make sure that nothing refers to the object and then the framework will do the work for you.


Constructor with IntPtr and JNIHandleOwnership


If you called Dispose() user peer of the object manually, and the Android OS needs this object, Mono will call the constructor shown below:


 public MyClass(IntPtr javaRef, JniHandleOwnership transfer) : base(javaRef, transfer) { } 

A similar constructor is in Xamarin.iOS only without JNIHandleOwnership. In this case, Mono tries to re-create the disappeared object.


If such a constructor is not implemented, your application will instantly NotSupportedException with a NotSupportedException . If Google decides to change the life cycle of an object and you call Dispose() before the end of this cycle, the application will also crash.


How WeakReference can help you


Use WeakReference instead of normal (strong) links in order to avoid placing a reference to native objects. It is a bit costly in searching these objects, but the native garbage collector can collect these objects at any time. Therefore, choose the type of links carefully! Bitmaps that cannot instantly disappear may be good candidates for weak links, but for small objects like UILabel this does not matter much.


What about Xamarin.Forms?


Each element from Xamarin.Forms has its own render on mobile platforms, either custom or supplied as part of the NuGet package. These renders are user peers and are treated as such. Here is an example of how Dispose() implemented in the built-in Android renderer. I would recommend sticking to a similar template when implementing your render and always dispose of native objects inside (see the code by reference).


Let Android and iOS help you.


Android and iOS have mechanisms that can alert you to an imminent lack of memory. In iOS, this DidReceiveMemoryWarning () in a UIViewController . On Android, this is more hidden and less documented: OnTrimMemory () in Application . It is logical to assume that you need to call GC.Collect() inside these methods. This will clear some objects, run several finalizers and call Dispose() on peer objects that are not used. This will allow the native garbage collector to clean up unused objects and free up more space on the native side.


Conclusion


I think that this post gives some useful recommendations on how to increase memory efficiency in your Xamarin applications, but pay attention, there is a lot more to tell about. I will talk about this in the next posts, and in the meantime, you can read the documentation, view the Xamarin sources on GitHub, or experiment with Xamarin or native profilers.


')

Source: https://habr.com/ru/post/330854/


All Articles