📜 ⬆️ ⬇️

Guide to background work in Android. Part 2: Loaders

This is the second of a series of articles on Android background tools and techniques. AsyncTask has already been reviewed earlier, in the following releases - ThreadPools with EventBus, RxJava 2 and Kortelins in Kotlin.



In the previous text, we mentioned that AsyncTasks have several problems. Let's recall two of them:


The meaning of the first problem is this: in order to update the UI in the onPostExecute method, we need a link to a specific view or to the whole Activity it belongs to. A naive approach is to store this link inside AsyncTask itself:
')
public static LoadWeatherForecastTask extends AsyncTask<String, Void, WeatherForecast> { private Activity activity; LoadWeatherForecastTask(Activity activity) { this.activity = activity; } } 

The problem is that as soon as the user rotates the device, the Activity is destroyed and the link becomes outdated. This leads to a memory leak. Why? Recall that our doInBackground method is called inside the Future, executed on an executor - a static member of the AsyncTask class. This makes our AsyncTask object, as well as an Activity, strictly achievable (because statics is one of the roots of GC), and therefore unsuitable for garbage collection. This in turn means that several screen rotations can cause OutOfMemoryError, because the Activity takes up a decent amount of memory.

You can fix this error using WeakReference:

 public static LoadWeatherForecastTask extends AsyncTask<String, Void, WeatherForecast> { private WeakReference<Activity> activityRef; LoadWeatherForecastTask(Activity activity) { this.activityRef = new WeakReference<>(activity); } } 

Well, we got rid of OOM, but the result of AsyncTask is lost in any case, and we are doomed to re-launch it, discharging the phone and expending traffic.



In order to fix this, the Android team several years ago proposed the Loaders API (“Boot Loaders”). Let's see how to use this API . We need to implement the Loader.Callbacks interface:

 inner class WeatherForecastLoaderCallbacks : LoaderManager.LoaderCallbacks<WeatherForecast> { override fun onLoaderReset(loader: Loader<WeatherForecast>?) { } override fun onCreateLoader(id: Int, args: Bundle?): Loader<WeatherForecast> { return WeatherForecastLoader(applicationContext) } override fun onLoadFinished(loader: Loader<WeatherForecast>?, data: WeatherForecast?) { temperatureTextView.text = data!!.temp.toString(); } } 

As you can see, the onLoadFinished method is very similar to onPostExecute , which we implemented in AsyncTask.

We need to create Loader itself:

 class WeatherForecastLoader(context: Context) : AsyncTaskLoader<WeatherForecast>(context) { override fun loadInBackground(): WeatherForecast { try { Thread.sleep(5000) } catch (e: InterruptedException) { return WeatherForecast("", 0F, "") } return WeatherForecast("Saint-Petersburg", 20F, "Sunny") } } 

And call initLoader () with our Loader id:

 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val weatherForecastLoader = WeatherForecastLoaderCallbacks() loaderManager .initLoader(forecastLoaderId, Bundle(), weatherForecastLoader) } 

Please note: WeatherForecastLoaderCallbacks is a nested class of our Activity; LoaderManager stores a link to this Callbacks object - and this means to the Activity itself.

A lot of code, right? But we get an important advantage here. First, the Loader is reused when the screen is rotated (or other configuration changes). If the screen is rotated, onLoadFinished will be called with the transfer of the result we loaded earlier.

Another advantage is that we don’t have a memory leak, although we still have access to the Activity, allowing us to update the interface.

Cool, AsyncTask did not have both of these advantages! Let's now figure out how it all works.



This is where the main fun begins. I thought that LoaderManager is stored somewhere inside Application, but the actual implementation was much more interesting.

LoaderManager is created when creating an instance of an Activity:

 public class Activity { final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); public LoaderManager getLoaderManager() { return mFragments.getLoaderManager(); } } 

FragmentController.createController is simply the named constructor for the FragmentController class. FragmentController delegates the creation of LoaderManager to HostCallbacks (a nested class of our Activity), the implementation looks like this:

 LoaderManagerImpl getLoaderManagerImpl() { if (mLoaderManager != null) { return mLoaderManager; } mCheckedForLoaderManager = true; mLoaderManager = getLoaderManager("(root)", mLoadersStarted, true /*create*/); return mLoaderManager; 

As you can see, LoaderManager for the Activity itself is lazily initialized; An instance of LoaderManager is not created until it is first needed. LoaderManager access to our Activity occurs via the '(root)' key in Map LoadersManagers. Access to this Map is implemented as follows:

 LoaderManagerImpl getLoaderManager(String who, boolean started, boolean create) { if (mAllLoaderManagers == null) { mAllLoaderManagers = new ArrayMap<String, LoaderManager>(); } LoaderManagerImpl lm = (LoaderManagerImpl) mAllLoaderManagers.get(who); if (lm == null && create) { lm = new LoaderManagerImpl(who, this, started); mAllLoaderManagers.put(who, lm); } else if (started && lm != null && !lm.mStarted){ lm.doStart(); } return lm; } 

However, this is not the last entry in the LoaderManager field. Let's look at the Activity # onCreate method:

 @MainThread @CallSuper protected void onCreate(@Nullable Bundle savedInstanceState) { ... if (mLastNonConfigurationInstances != null) { mFragments.restoreLoaderNonConfig( mLastNonConfigurationInstances.loaders); } ... } 

The restoreLoaderNonConfig method ultimately simply updates the host controller, which is now a member of the class of the new Activity instance created after the configuration change.


When calling the initLoader () method, LoaderManager already has all the information about Loaders that was created in the destroyed Activity. So he can publish the downloaded result immediately:

 public abstract class LoaderManager { public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) { ... LoaderInfo info = mLoaders.get(id); ... if (info == null) { info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback); } else { // override old callbacks reference here to new one info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>) callback; } if (info.mHaveData && mStarted) { // deliver the result we already have info.callOnLoadFinished(info.mLoader, info.mData); } return (Loader<D>)info.mLoader; } 

It's great that we dealt with two things at once: how we avoid memory leaks (replacing the LoaderCallback instance) and how we deliver the result to a new Activity!

Perhaps you are wondering what else this beast is for mLastNonConfigurationInstances. This is an instance of the NonConfigurationInstances class, defined inside the Activity class:

 public class Activity { static final class NonConfigurationInstances { Object activity; HashMap<String, Object> children; FragmentManagerNonConfig fragments; ArrayMap<String, LoaderManager> loaders; VoiceInteractor voiceInteractor; } NonConfigurationInstances mLastNonConfigurationInstances; } 

The object is created using the retainNonConfigurationInstance () method, and then it is directly accessed by the Android OS. And it becomes available for Activity in the Activity # attach () method (and this is the internal Activity API):

 final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback) { attachBaseContext(context); ... mLastNonConfigurationInstances = lastNonConfigurationInstances; ... } 

So, unfortunately, the main magic remains inside the Android OS. But no one will forbid us to learn from her example!


Let's summarize what we found:

  1. Loaders have a non-obvious API, but they allow you to save the download result when the configuration changes
  2. Loaders do not cause memory leaks: just don't make them nested classes. Activity
  3. Loaders allow you to reuse AsyncTask in a background job, but you can also implement your own Loader
  4. LoaderManager is reused between deleted and newly created Activity due to saving to a special object.

In the next article we will talk about how to organize background work on Executors, and mix in a bit of EventBus. Stay tuned!
Minute advertising.
From the author of the text:

As you noticed, this is a translation of my English-language article . If the article seemed valuable to you, pay attention - in April there will be a Mobius conference, in whose program committee I enter, and I can promise that this year the program will be particularly rich. Only a part of the program has been published on the conference site so far, because it is extremely difficult for us to choose the best ones - the competition is more acute than ever. You can already study the existing descriptions of the reports, and soon new ones will be added to them!

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


All Articles