Crashlytics Logs With ‘Impossible’ NullPointerExceptions

Android process death — what every mobile dev ought to know

Eric Silverberg
The Startup
8 min readFeb 19, 2021

--

Who is that mysterious culprit? Photo by Carlos Alberto Gómez Iñiguez on Unsplash

Most Android developers understand that their Activity can be killed at any time when it is in the background. Indeed, this is one of the critical features provided by any operating system.

Most Android developers have also probably used Crashlytics and seen stacks with seemingly “impossible” NullPointerException crashes, right around the start of an Activity or Fragment. Try it — go launch https://console.firebase.google.com and see how far down you must scroll before you see an exception matching this pattern. I bet it’s not far!

The solution to this class of problem starts first with a firm understanding of Android Process Death. This is a topic with some buried discussion on Google’s website and a smattering of blog posts, so in this piece we are going to shine a Bright Spotlight on this issue once and for all.

Let’s start by discussing how apps get launched on Android.

Launching by Home screen

A familiar sight

When a user returns to an app that has previously been killed by the operating system while it was in the background by tapping on an icon from the home screen of Android, the app reinitializes itself via a standard launch sequence — the Application is created then the primary Activity (defined in the manifest file with android.intent.category.LAUNCHER) is created.

This is among the most common ways to launch and app. It likely should work fine for you and your app.

Launching by the Recents screen

Easy to switch, easy to crash.

When a user returns to an app killed by the operating system by using the Recents screen (accessed by tapping the square icon □ at the bottom of a device), an entirely different sequence unfolds:

The system will try to recreate the Activity the user was previously in without going through the LAUNCHER activity. In our apps, we perform our local database and remote API initialization in a SplashActivity, because we want to show the user an animated spinner while we are setting up the app — we don’t want the user to see a blank or static screen during this process. We initialize our application-scoped repositories here, and expect these to persist during the lifecycle of the application.

In this above scenario, because Android immediately recreates the Activity without going through the standard LAUNCHER flow, every piece of data that is application scoped is unavailable when this inner Activity is created.

Why not initialize Repositories in the Application class?

So true JT. Also, #FreeBritney

Good idea! How? Any “serious” initialization is going to have some kind of I/O component, whether reading from network or (more likely) reading from SQLite on disk. Will you block the main thread while you wait for your local database to be warmed? (See Strict Mode Violation). Or, will you hope that background threads which connect to your local SQLite database finish fast enough so that your data is warmed and ready in time for the newly restored Activity. (See Race Condition).

What does this look like in production?

Let’s start with some Crashlytics logs. Normally when investigating a crash, you would see something like the following:

You can see the SplashActivity is the first Activity that gets created, following with a Session started log entry. Later on (not pictured), the HomeActivity is created, then our fragments, and so on. This standard sequence can be compared against other crash logs to help narrow down the issue.

Now let’s take a look at the following crash log:

We would have expected to see log entries about the SplashActivity similar to the log above, but no such entries exist. How is that possible, and how can the HomeActivity get initialized without going through the SplashActivity? Deep links perhaps? That still doesn’t make sense — if it was a deep link, you would expect more than one fragment of the HomeActivity to get created and appear in this log, since we’re parsing deep links after our HomeActivity finishes initialization.

It turns out that this is a case of the infamous Android process death.

The 3 scenarios for restoring an application

Android developers probably already know that when an app is in the background, it can get killed at any time by the system, depending on the available memory of the system, the battery level, the priority of other apps that have been backgrounded, whether doze mode is enabled, and so on. But by saying that the app was “killed” what exactly does it mean? What exactly gets killed? We have 3 possible scenarios when restoring an app from the background:

Scenario 1: Activity Alive 🙂, Application Alive 🙂

The app is still fully preserved by the system:

Activity

Application

Bundle

That’s the happy scenario; the user will instantly see the same screen he was in when he backgrounded the app because nothing got killed by the system. Probably the system had enough resources and didn’t have to clean anything up.

Scenario 2: Activity Dead 😵, Application Alive 🙂

The Activity was killed, but the application is still alive:

❌ Activity

Application

Bundle

This is the not so happy scenario, since the Activity was killed and it will need to be re-created when the user foregrounds the app. This can be emulated by enabling the “Don’t keep activities” setting in the developer options. In most cases, this is still a fine scenario since the application stayed alive so the Activity can instantly access any application-scoped data it needs.

Scenario 3: Activity Dead 😵, Application Dead 😵

Activity and Application are gone. Only the Bundle remained alive:

❌ Activity

❌ Application

Bundle

This is the sad scenario where things can go wrong. Not only the Activity was killed, but we also no longer have access to any application-scoped resources. And that’s exactly what the “process death” is. It’s the Android application process that got killed. We only have a Bundle, which is the stored state of the Activity right before it got killed (that Bundle object we are getting in onCreate()).

The key thing to note here, is that when the app gets foregrounded, the system will try to re-create the Activity as soon as possible to prevent any latency that might be perceived by the users. And why is this bad? Because every object that is application-scoped will no longer exist while the Activity is created. Anything that is stored in a singleton or injected dependencies, mutable static fields, or data that is stored in our Application class are now gone. Basically anything that we don’t explicitly store in onSaveInstanceState() will be gone.

Which causes a big pain in our app, since we rely on the user’s profile in almost all features of the app. The user’s Profile is first read from the database as part of our initialization logic in our SplashActivity and then stored in memory in an application-scoped AccountRepository. In order to find these bugs, we made that Profile non-optional, and we throw UninitializedPropertyAccessException if it has not yet been set:

Ideas to prevent crashing when accessing data from repositories

If you read blog posts you may find the following ideas:

Don’t use singletons

Easier said than done! It’s not practically possible to share data between different areas of the app without the repositories being single-instance application-scoped objects.

Finish activities early

We could also just finish the Activity early while it’s being re-created, but that feels like we’re treating the symptoms of the problem and not the root cause. Not to mention that trying to finish an Activity before calling super.onCreate() will crash the app.

Store data in the Bundle object

Another option would be to store the Profile in the Bundle object in onSaveInstanceState() but that’s not practical either. We can’t save and restore all the data that we need from our repositories in every Activity. There’s also a hard limit on the amount of data we can store in a Bundle.

Ignore it as an edge case

Well, that’s what we did at first. We expected the Android process death to be a rare scenario, but our Crashlytics numbers proved us wrong. Not only these issues diminish the user’s experience, but a higher crash rate will likely lead to more support tickets and negative app store reviews. Not to mention the “bad behavior threshold” of Google Play, which will basically punish the app by lowering the app ranking if it crashes a lot.

So if none of these ideas were practical, what did we do?

Design a better architecture

Google doesn’t always have the right answers!

Our approach to fight this issue was to design an Activity Lifecycle that fits in an MVVM world and that won’t try to access data that haven’t been initialized yet, guarding us against the Android process death scenario.

Next up

Learn about our Clean MVVM Activity Lifecycle for resolving these issues.

Further reading

Other series you might like

Clean API Architecture (2021)
Classes, execution patterns, and abstractions when building a modern API endpoint.

Kotlin in Xcode? Swift in Android Studio? (2020)
A series on using Clean + MVVM for consistent architecture on iOS & Android

About the authors

Eric Silverberg and Stelios Frantzeskakis are developers for Perry Street Software, publishers of the LGBTQ+ dating apps SCRUFF and Jack’d, with more than 20M members worldwide.

--

--

Eric Silverberg
The Startup

CEO, Perry Street Software. Developer. 🏳️‍🌈