Android Memory Management and Save & Restore

//Android Memory Management and Save & Restore

In this post I will describe one of the core memory management mechanisms inside Android OS.

This mechanism has tremendous implication for Android applications and, as such, should be understood in details by professional Android developers.

Memory limitation on mobile devices:

Let’s start with a description of the motivation behind “save and restore” mechanism which will be discussed in details later.

To explain this motivation I will compare mobile devices and applications to their PC counterparts.

Mobile devices are physically small and work on battery. Therefore, they are constrained in the amount of available RAM memory and CPU processing power. In addition, mobile devices should make an efficient use of electrical energy to prolong battery life.

However, mobile applications, on average, have memory demands which are comparable with their PC counterparts.

For example, comparing memory consumption of the same brand on my mobile phone and desktop, I usually see a factor of 2-4 in memory consumption. But my desktop has 8 times more memory than my mobile phone and this memory is much faster.

This “on average” comparison is not applicable to performance hungry applications like 3D games and etc., but, in my empirical survey, it was applicable to most of “regular” applications which are not graphics heavy.

So, the situation on mobile devices is that there is less RAM memory available, but applications demand a comparable amount of it.

In addition, users use more applications on mobile devices.

In their latest report, AppAnie indicated that Android users on average use 10 applications per day. I couldn’t find a comparable statistics for PC, but I would estimate that the number for PCs is at most half of that for mobile. I won’t be surprised if the actual average number of applications used per day on PCs is 2-3.

In order to perform this estimation I made a list of how many PC applications I use daily. This list contains exactly 10 applications, one of which is Windows File Explorer. Furthermore, 7 out of 10 are directly related to my job as a professional software engineer. Therefore, if my job would not involve interaction with PC, or that interaction would be simpler, I would probably use just 3-4 desktop applications on a daily basis.

In addition, there are days when I don’t touch my PC at all, but I can hardly imagine a day without my phone. Even when I go to a desert for a week I make sure to bring a nice power bank with me.

So, this is the situation: users use more applications on mobile phones, each of which consumes a comparable amount of RAM on average, but mobile phones have less RAM available. This is a big issue that needs to be addressed.

There are several approaches that could be employed.

Single application operating system:

The simplest approach would be to allow just one single application to be in memory at any given instant and shut down and clear from memory all others.

While this approach is simplest to implement, in practice such an OS would be horrible in terms of user experience.

Users would face three problems:

  1. Switching between application would take much time because the new app would need to be started from scratch and loaded into memory.
  2. The state of the previous application would be lost.
  3. No application would be able to provide background functionality.

I don’t know for sure, but it might be the case that MS-DOS was the last “operating system” that operated in this fashion.

This approach is simply not practical. Let’s move on.

Multiple applications operating system:

Allowing multiple applications to be executed concurrently is what operating systems do in general.

However, when multiple applications are alive, each of them consumes some memory. Due to the reasons discussed earlier, mobile devices will quickly run out of memory as users switch between applications. Therefore, management of applications memory consumption becomes a necessity.

Depending on the degree of user involvement, memory management can be automatic, semi-automatic and manual.

Android implements an automatic memory management system in which users are not involved in routine operations. This memory management scheme boils down to two aspects:

  1. Android limits the amount of memory a single application can consume and terminates violators.
  2. Android keeps track of total memory consumption and terminates applications when it runs low on memory.

The component that OS uses in order to terminate applications when memory consumption becomes too high is called “low memory killer”.

The exact algorithm that low memory killer uses is not documented at all. Maybe it can be reverse engineered form AOSP source code, but I didn’t go that far myself, and I doubt that this information is relevant to most applications.

So, using this approach Android allows for multiple applications to reside in memory and, if needed, even execute in background. If application misbehaves and attempts to claim more memory than allowed then Android will kill it. If the total memory consumed by all applications grows beyond some threshold then Android low memory killer will kick in and terminate some of them.

Users get a nice and smooth experience of switching between applications because many of them remain in memory and are simply brought to foreground when “launched”.

All good, but there is still one “small” problem. When low memory killer terminates applications, the non-persistent state of those applications is lost.

For example: a user could have been writing a long body of text in application and then had to switch to some other application. If low memory killer terminates the now backgrounded application before the user gets back to it and has a chance to complete and store the text, then user’s progress will be lost. This is really bad user experience.

User generated text is just an example of a non-persistent state of the application that is lost when low memory killer kicks in. In some cases, clearing the non-persistent state might be acceptable and even desirable, but in other cases the non-persistent state must not be lost (e.g. user input).

This issue must be addressed.

Multiple applications operating system with “save and restore”:

So, each application might be terminated by Android’s low memory killer at some point in time and loose its non-persistent state. However, part of this state mustn’t be lost.

The solution to this problem is to save parts of non-persistent state in a persistent storage right before the application is killed, and then restore the saved state when the application is brought back to life.

This way, even though the application was terminated, when a user will get back to the application later its overall state will be equivalent to the state it had when the user left it. In other words – the user will not be aware of the fact that the application was terminated due to system low memory conditions between interactions (except for longer launch time).

This flow of persisting parts of non-persistent state before applications termination and restoring it later is called “save and restore”.

Technically speaking, I don’t know whether Android actually stores applications’ saved state in persistent storage (e.g. on disk). My guess would be that this state is still kept in memory, but this memory belongs to Android OS and is not accessible to low memory killer.

In practice, however, when looked from application’s perspective, it is irrelevant whether the saved state is actually persisted or not. The only thing that matters for the application is that it will have a chance to restore its previous state next time the user interacts with it.

The crucial point here is that save and restore flow is not handled automatically by Android on its own. The operating system controls when and how the flow is being executed, but it has no means to know which exactly parts of application’s state should be saved. Therefore, it is up to applications’ developers to save the state when the system initiates save and restore flow and restore it later when the application is re-created.

It is also important to know that saved state is not kept upon device reboots. Therefore, if the device is rebooted, the next time the user navigates to any application it will be started from scratch.

This is important because some user input might be that much valuable that we, as application developers, would want to protect it from device reboots as well. In this case Android OS doesn’t help us much and it becomes our sole responsibility to manually persist the critical state into a persistent storage.

However, the scenario of manually saving the state in persistent storage is outside the scope of this post.

Save and restore is users everyday reality:

One myth that many Android developers believe is that save and restore doesn’t happen often.

The problem with this specific myth is that it makes developers less attentive to the nuances and implementation details of save and restore mechanism. This is very unfortunate, given the fact that most Android users experience save and restore on a daily basis.

I make this claim confidently because I tested it.

On my Galaxy S4 with 2GB or RAM I can easily make background applications being killed by OS. All it takes is to open 8-10 applications with above average memory consumption.

In fact, I reproduced save and restore just now by opening the following 8 apps (just in case – I’m not affiliated with any of them): Waze, Facebook, Gmail, Gett, Wunderlist, Evernote, Google Maps and the memory monitor in Settings. My phone can’t keep all these applications in memory at once, so only 7 out of 8 can be alive at any instant.

Given the fact that users use 10 applications a day on average, all the users who own devices with 2GB of memory or less will experience save and restore on a daily basis. Probably much more than once because the users are also likely switching back and forth between applications.

So the question now becomes how many users actually own such devices. The answer is that most of the users do.

Take a look at the below charts that I copied form DeviceAtlas Mobile Web Intelligence Report for Q2 of 2017. These charts show the market share for devices having different amounts of RAM:

Most common RAM amounts 1

Most Common RAM Amounts 2

From these charts it is clear that ~40% of the users worldwide have devices with 2GB of RAM. But this stat becomes completely irrelevant when you realize that ~30% of the users have devices with just 1GB of RAM.

I don’t have 1GB device for testing, but I would estimate that on such devices low memory killer kicks in after 3-5 applications are launched.

Since ~30% of users have this kind of devices, you should probably develop applications with this number in mind (unless you target a geographic or economic niche of users).

But even this is not the entire picture. This chart from the same report shows the countries with the highest volume of devices having 512MB of RAM:

Share of Phones with 512MB RAM

And that’s the point when we, Android developers, can become legitimately depressed.

Note: these charts do not contain statistics for China.

In summary, it looks like more than 70% of Android users will experience save and restore on a daily basis.

Therefore, if the application is being used at all, it is probably subject to save and restore much more often than many Android developers would think.

A word about configuration changes:

At this point I would like to discuss configuration changes just a bit. What I’m going to share with you now is just an opinion of mine and it is not based on any large scale empirical research. Hopefully it will give you an interesting perspective.

Or you can totally skip to the next section because familiarity with this opinion is not required in order to understand the rest of this post.

In the past years there was a hype around optimization of configuration change flow in Android applications, without optimization of save and restore flow. I’m talking primarily about Loaders framework and ViewModel “architectural component” promoted by Google (well, Loaders have already been effectively deprecated, but they were all the hype when I got into Android development).

I find this hype artificially created and very bad for Android users.

In my opinion, it is created artificially because, on average, there is no justification for optimization of configuration changes. Such an optimization should be done on case by case basis, after there is hard data available that suggests that it is required.

Otherwise, it is “premature optimization” all over:

There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified.Structured Programming with go to Statements; 1974; Donald E. Knuth

I think that this hype is bad for Android users because it wasted time and attention of development community. Instead of concentrating on stuff that really matters, the community is being constantly steered into useless churn by quite impressive Google’s PR budget.

If you go through the above numbers, you will realize that optimizing configuration changes without optimizing save and restore is completely wasteful activity on average.

For example, think about all the applications locked to portrait mode. They will rarely ever experience configuration change, but will still be subject to save and restore flow on a daily basis for the active users.

And just to make it crystal clear – I’m not saying that we should not support configuration change. I’m saying that we should not optimize configuration change flow unless there is hard data that suggests that such an optimization is required.

The above was my attempt to raise your awareness to “premature optimizations” related to configuration change handling.

Let’s get back to save and restore now.

Single Activity save and restore:

In order to understand how save and restore in Android should be handled with framework’s assistance, it is enough to understand what happens to individual Activities. Fragment’s save and restore is very similar, and Services do not get any assistance from the framework at all, such that with Services you’re on your own.

This last piece always puzzled me – why developers of Android decided not to support semi-automatic save and restore for Services.

Anyway, let’s discuss what happens to Activities.

Right before the application is about to be terminated by low memory killer, each Activity is notified about this with a call to its onSaveInstanceState(Bundle) method.

In fact, it is not exactly guaranteed that the application is going to be killed if this method is called, but we, as professional Android developers, should assume that every single call to this method is our last chance to save the state.

In the body of this method we call through to super implementation, and also put all additional state we are interested in into the provided Bundle.

Why we put “additional state” and not all state?

Because the call to super implementation automatically saves much of the “standard” state for us. This includes the state of all standard sub-classes of View class in the view hierarchy of this specific Activity, the state of Activity’s FragmentManager, etc.

We only need to manually save additional state that Android framework is not aware of.

For example, this method could look like this for Activity in which the user places a complex order:

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putBoolean(SAVED_STATE_FIRST_TIME_PROMOTION_SEEN, mFirstTimePromotionSeen);
        outState.putSerializable(SAVED_STATE_ORDER_IN_PROGRESS, mOrderInProgress);
    }

(you might wonder why I use Serializable instead of Parcelable – let’s say it is spoiler for another discussion)

If there is no special state that needs to be saved, this method can be omitted.

So, after this method returns we can be rest assured that all the state required for proper re-creation of this specific Activity was saved.

Later, after the application had already been saved and destroyed, and the user launched it again, the system will initiate the restoration of application’s state. It will re-create the Activity that was last shown to the user and initiate restoration of that Activity’s state.

From Activity’s point of view it means that the Bundle which was saved after onSaveInstanceState(Bundle) returned, will be passed into onCreate(Bundle) method.

Then we can use this Bundle to get all the saved state back into the new instance of Activity:

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState != null) {
            mFirstTimePromotionSeen =
                    savedInstanceState.getBoolean(SAVED_STATE_FIRST_TIME_PROMOTION_SEEN);
            mOrderInProgress =
                    (Order) savedInstanceState.getSerializable(SAVED_STATE_ORDER_IN_PROGRESS);
        }
    }

At this point, if you correctly identified all the state that needs to be included in save and restore flow, the state of this Activity should be equivalent to the state of the Activity destroyed by low memory killer.

The user will just get back to the same screen in the same state and will be able to continue whatever he/she was doing like the application has never been terminated.

Save and restore flow of a single Activity is drawn on this UML diagram:

android_save_restore

The “pre-save” instance of the Activity receives calls to onSaveInstanceState(Bundle) and later to onDestroy(), at which point this instance “dies” and can be garbage collected. Some time after that a new instance of the same Activity is created, and the Bundle containing saved state is being passed into its onCreate(Bundle) method.

It is our responsibility to ensure that all the state (except for the “standard” described above) is being properly saved and restored.

Multiple Activities save and restore:

Hopefully you understand how save and restore operates on a single Activity level now (or already knew it before).

The more interesting and much less documented aspect is how save and restore works in case of multiple activities on the back-stack.

If low memory killer decides to terminate an application with multiple activities on the back-stack, the following will happen:

  1. For each individual Activity on the back-stack onSaveInstanceState(Bundle) will be called.
  2. After the state of each individual Activity is saved, Android will automatically save the sate of the back-stack itself.

The state of the back-stack contains all the information required in order to reconstruct equivalent “chain” of Activities upon future state restoration. This includes the type of the Activities on the back-stack, their order, Intents that started the Activities, etc.

In short: we are responsible for saving the state of each individual Activity; Android saves the relationship between them.

Then the application is killed. At some point in the future the user will launch the application again, and this will result in the following flow:

  1. Android will automatically restore the state of the back-stack.
  2. An instance of the topmost Activity on the back-stack will be created, and its onCreate(Bundle) method will be called with Bundle containing all the saved state for that specific Activity.

Did you notice that something is missing? What about all other Activities on the back-stack except the topmost one?

Turns out that Android doesn’t eagerly instantiate all Activities that were on the back-stack prior to save and restore flow. It instantiates only the topmost one and lets it restore its state, and then does nothing.

Only if the user navigates “back” in back-stack will Android instantiate the previous Activities and allow them to restore their state.

An example will be appropriate now.

Imagine this situation: user launched the app and ActivityA was shown. Then he/she navigated to ActivityB and then to ActivityC. Then left the application.

Now activities back-stack contains three “live” activities, including ActivityC at the top of the stack, which is the one the user saw the last.

Low memory killer kicks in and wants to terminate this application. Android will call onSaveInstanceState(Bundle) for each of the three Activities, automatically save the state of the back stack and allow low memory killer to do its job.

Later the user launches the same application.

Android restores the state of the back-stack and realizes that ActivityC is the topmost one. An instance of ActivityC is created and its onCreate(Bundle) method is called with Bundle containing its saved state.

A record of ActivityC is removed from the back-stack at this point, such that ActivityB becomes the topmost one.

At this point restore flow is completed and the user will be able to interact with the application.

If the user decides to navigate back now, then Android will instantiate ActivityB, let it restore its state and then remove ActivityB record from the back-stack such that ActivityA becomes the topmost one.

Note that even though the user had already been interacting with the application for some time, the act of simple back navigation caused a restore flow to be executed at Activity level.

Now you can try to formulate yourself what will happen if the user navigates back again.

Summary:

Let’s summarize what was discussed in this article.

I started with description of the motivation behind save and restore flow:

Due to limited amount of memory on mobile devices and quite high expected demand by applications, low memory killer component had to be introduced. It terminates applications in order to free RAM, which allows the OS to support execution of multiple applications in parallel while not bothering the user with explicit memory management activities. However, in order to prevent state loss upon applications termination by low memory killer, all applications had to support save and restore flow.

After the general discussion of save and restore I tried to convince you (and, hopefully, succeeded) that majority of Android users experience save and restore flow on a daily basis. Probably much more than once a day on average.

Then we discussed the actual implementation of save and restore at the level of an individual Activity, followed by generalization to multiple saved activities on the back-stack.

The interesting realization from this discussion was that save flow is executed in one pass, while restore flow executes “lazily” – one Activity at a time.

That’s all for this post. The introduction is complete.

You heard me right, introduction.

The plan was to write an article about how to actually test that applications properly handle save and restore flow, but then I noticed that the introduction got quite long and decided to extract it into a separate article.

In the next post I will hopefully get to the testing part.

As always, you’re welcome to leave your comments and questions below, and consider subscribing to our newsletter if you liked this post.

Leave A Comment

Stay Connected!

Receive notifications about new posts and resources straight into your inbox!
SUBSCRIBE
close-link