Today we bring to your attention a little material about the competent use of memory in Android.
This article focuses on the basic techniques for managing memory usage in applications — for example, browsers, photo editors, and PDF viewers — in which large memory requests are made.
First, a little theory
Most Android apps run on top of the runtime (ART) that replaced the now obsolete Dalvik virtual machine. ART and Dalvik are similar to the Java Virtual Machine (JVM), with which they share similar design principles. They use two separate spaces to store application data: the stack and the heap.
The Java stack stack is used to store local variables (primitive types and object references). Each Java thread has its own separate stack. Stack memory is relatively small compared to heap memory. Dalvik’s Java stack size is typically 32 KB for Java code and 1 MB for native code (C ++ / JNI). In ART, a unified stack for Java and C ++ has appeared, the size of which is about 1 MB.
When the application selects the entire stack memory to the limit, an error is issued
StackOverflowError. The most likely reasons a stack limit can be reached is either infinite recursion or an overly deep method call. References to the stack memory are always made in the LIFO order (last come – first served). Each time a method is called, a new frame with local variables of this method is pushed onto the stack. When the method completes, its frame is popped off the stack, and any possible resulting value is sent back to the stack. So, the first problem (infinite recursion) is a bug that is easy to fix, but the second requires some refactoring, which consists in deploying recursive method calls and converting them into a loop.
Heap memory in Java is used by a virtual machine to allocate objects. Whenever an object is created, it happens on the heap. Virtual machines, such as the JVM or ART, regularly collect garbage, remove all objects that are no longer referenced, and thus free up memory to allocate new objects.
To ensure usability, Android tightly limits the heap sizes for each running application. The heap size limit varies from device to device and depends on how much RAM is on that device. If your application reaches the maximum heap size and tries to allocate more memory, an error is issued
OutOfMemoryError, and the application ends. Let’s look at some examples to help avoid this situation.
Heap memory analysis
The most important tool for understanding memory problems in your applications and understanding how memory is used is memory profileravailable in Android Studio.
This tool visualizes how much memory your application consumes over time. You can take snapshots of the Java heap in a running application, record the memory allocation operations, and monitor the heap or this memory allocation history in a powerful UI.
A typical memory profiler session should look like this:
- We look at the most frequent memory allocations and garbage collector passages to identify possible performance problems.
- We look at how memory was used over time, especially those operations for which, as you know, you need to allocate a lot of memory. Make sure that after completing these operations, memory usage is reduced. For example, the following shows how activity affects memory.
PdfActivityof PSPDFKit after downloading the document.
- We make a heap dump at different times during the execution of your application and check how memory is used. We are looking for large objects that are stored in memory and do not fall under garbage collection. Heap dumps also help identify memory leaks – for example, you can search your activity in a heap dump and see if their old instances were collected.
Modern garbage collectors are complex works of technological art, the result of many years of research and development, in which hundreds of people participated, from academics to professional developers. However, you still have to be on the alert to prevent memory leaks.
Exemplary Memory Leak Detection Solution – Library Leakcanary. It automatically issues notifications when in your test build (development build), giving you the leak trace rate in the UI of this program. You can (and should) integrate her today, especially since it’s not difficult!
It is especially easy to provoke memory leaks when working with complex life cycles of Android activities or fragments. This often happens at points where developers hold strong links to contexts UI or other UI-specific objects in a background task or in static variables. One way to provoke such delays is to actively twist the device when testing your application.
Free up memory in response to events
Android may require the app to allocate allocated memory, or simply force it to end when memory needs to be freed up for more critical tasks. Before this happens, the system will allow you to give all the memory that you do not need. In your activity you will need to implement an interface
ComponentCallbacks2. In this case, whenever your system runs out of memory, a call will be made to your method
onTrimMemory(), and you will be able to free up memory or disable features that will not work in such a shortage of memory.
So, such callbacks are handled in the PSPDFKit application. The PSPDFKit application was designed with the calculation of the active use of memory for caching, so that the work with the application goes as smoothly as possible. Initially, it is not known how much memory is available on the device, so PSPDFKit adapts to the situation and limits the use of memory when it receives notifications that there is not enough memory. Therefore, applications integrated with PSPDFKit work even on low-tech devices, but with reduced performance due to the fact that caching is disabled.
One of the front-end solutions to cope with high memory requirements is to request a large bunch of Dalvik for your application. You can add
android:largeHeap="true" to tag
If for a property
largeHeap set value
true, Android will create all processes for your application with a large heap. This setting is intended only for those applications that by their nature cannot work without it, that is, they use voluminous resources that must simultaneously fit in memory.
It is strongly discouraged to use a large heap if you only want to raise the ceiling for possible memory usage. Memory usage should always be optimized, since even a large pile of your application may not be enough when working on a weak device with small memory.
Check how much memory your application can use
It never hurts to check how large the heap of your application is and to dynamically adapt your code and available capabilities to these memory limits. You can check the maximum heap size directly at runtime using methods
getLargeMemoryClass() (when a large heap is included).
Android even supports devices with only 512 MB RAM. Be sure not to ignore low-tech devices! Using the method
isLowRamDevice() You can check if your application is running on such a device where there is not enough available memory. The exact behavior of this method depends on the device, but it usually returns true on devices with less than 1 GB of RAM. You need to make sure that your application works correctly on these devices, and disable all features that use a large amount of memory on them.
You can read more about how Android works on low memory devices. here; Additional optimization tips are also provided here.
Use optimized data structures
In many cases, applications use too much memory for the simple reason that they do not use the most appropriate data structures.
Java collections cannot store efficient primitive types and require packing their keys and values. For example,
HashMap with integer keys should be replaced with optimized ones
SparseArray. Ultimately, you can always use raw arrays instead of collections, and this is a great idea if your collection is not resizable.
Other data structures that are inefficient in terms of memory usage include various serializations. Yes, indeed, XML or JSON formats are convenient to use, you can reduce memory usage if you work with a more efficient binary format, for example, protocol buffers.
All of these examples, with data structures optimized to save memory, are just hints. As with refactoring, you must first find the source of the problems, and then move on to such performance optimizations.
Prevent memory shuffling
Java / Android virtual machines allocate objects very quickly. Garbage collection is also very fast. However, when allocating a large number of objects in a short period of time, you may encounter a problem called “memory churn”. In this case, the virtual machine will not have time to allocate objects at this pace, and the garbage collector will dispose of them, and the application will begin to slow down, and in extreme cases it will even use up all the memory.
The main problem on Android territory in this case is that we do not control when garbage collection will occur. Potentially, this can lead to problems: for example, the garbage collector works exactly at the time the animation unfolds on the screen and we exceed the threshold of 16 ms, which ensures smooth display of frames. Therefore, it is important to prevent over-allocation of memory in the code.
An example of a situation leading to memory shuffling is the allocation of large objects, for example, Paint inside a method
onDraw() representation. In this case, many objects are quickly created, and garbage collection can begin, which can adversely affect the operation of this view. As mentioned above, you should always monitor memory usage to avoid such situations.
Random access memory (RAM) on mobile devices can be a very limited resource. Ensuring efficient use of memory in the application is especially important if your application works with relatively large objects, for example, raster graphics (PDF viewers, web browsers, photo editors) or large media files (audio or video editors). Following these tips, you will learn how to create high-quality applications that will work at an acceptable level, even on the most powerful devices.