This is the second part of the series about tools and approaches for Android background.
In the previous one we mentioned AsyncTasks have several issues with them. Let’s recall a couple of those issues:
- AsyncTasks don’t know anything about the Activity lifecycle and therefore may introduce memory leaks at best and a crash at worst if used incorrectly.
- AsyncTasks don’t support progress or result reusage.
Taking a closer look on the first one, we need to cope with the following issue: as we want update the UI in onPostExecute method we need a reference to a particular view or an entire Activity the view is inflated to. The naive approach is to store that reference inside the AsyncTask itself:
The problem here is that once the user rotates the device the Activity gets destroyed and the reference becomes obsolete. This leads to a memory leak. How? Remember, that our doInBackground method is invoked within a Future executed on the static member of AsyncTask class, which makes our AsyncTask instance strongly reachable from the GC root(and the activity as well), thus not eligible for garbage collection. That means that several screen rotations causes OutOfMemoryError due to relatively large memory chunk occupied by the Activity object.
The fix to this OutOfMemory issue is a usage of WeakReferences:
Well, we got rid of OOM but the result of an AsyncTaskis lost anyway and we are doomed to launch our AsyncTaskagain draining the battery and data plan.
The Android team suggested Loaders API to address the issue several years ago. Let’s check out how to use that API. We need to implement a Loader.Callbacks interface:
As you may notice the onLoadFinishedis really similar with onPostExecute we were implementing with AsyncTask.
We need to create a loader itself:
And call initLoader() with the our Loader id:
Please, take an important note: WeatherForecastLoaderCallbacks is an inner class of our Activity; LoaderManager stores the reference to this callbacks instance meaning storing the reference to the Activity itself.
Plenty of code, right? But we gain an important advantage here. First of all the instance of the Loader gets reused during screen rotation (or other configuration changes).
Once you rotate the screen the onLoadFinished will be called with the cached result.
Another advantage we got is that we don’t have a memory leak due to saving Activity instance still being able to update the UI.
Cool, the AsyncTasks didn’t have either of that features!
Let’s figure out how that works.
The fun part begins here. My thought was LoaderManager gets stored somewhere inside Application but the actual implementation appeared to be much more interesting.
LoaderManager is created when the Activity gets instantiated:
FragmentController.createControlleris just a named constructor for FragmentControllerclass. FragmentControllerdelegates the loader manager creation to HostCallbacks(which is an inner class of our Activity) which implementation is the following:
As you see the LoaderManager for Activity itself is lazy-initialized; the LoaderManager is not instantiated until it is requested for the first time. Activity’s loader manager has a ‘(root)’ tag as a key in the LoadersManagers map. Access to that map is implemented like this:
However, this is not the final initialization of loader manager which happens. Let’s take a look into Activity#onCreate method:
restoreLoaderNonConfig ends up with just updating the host controller, which now is a property of our new Activity, created after configuration change.
Once you call initLoader() method the loaderManager already has the info about all the loaders which were created in the destroyed activity. So it has an ability to publish the loaded result immediately:
Great that we figured out two things at once: how we avoid memory leaks(by replacing the LoaderCallbackinstance) and how we deliver result into a new Activity!
The question you possible have is what the hell is mLastNonConfigurationInstances. This is an instance of a NonConfigurationInstancesclass, defined inside the Activity class:
This object is created with retainNonConfigurationInstance() method and then accessed by the Android OS directly. And it begins to be available to the Activitywithin Activity#attach()method(which is an Activityinternal API as well):
So unfortunately, the main magic stays inside the Android OS itself. But it doesn’t block us from learning!
Retaining and restoring the objects during configuration change is still the magic of Android OS itself
Let’s summarize a little bit what we just discovered.
- Loaders have not obvious api, but allow to save the loading result across the configuration change
- Loaders don’t introduce memory leaks: just remember not to make them Activity’s inner classes
- Loaders offer a possibility to perform background work reusing AsyncTasks but you are free to implement your own Loader
- LoaderManager gets reused between destoyed and newly created activity through saving the actual instances in a special object.
See you in the next part about EventBus!