Pages

Tuesday, October 18, 2016

Android's bad company: IntentService, ResultReceiver and Configuration Changes

| Intro |

I has a simple task: convert the given location (latitude and longitude coordinates) into the human-readable address strings (city, street, post code etc.). On iOS platform this could be solved with the help of just several lines of code. On Android it turned out to be a complicated and error-prone problem with a lot of pitfalls.

At the first glance, this task can be very easily accomplished using the official code sample. However, since we don't live in a perfect world, this sample is not suitable for the production code. It may look good for some Android newcomers but if we have a goal to develop a real-world app a lot of different edge-cases (as much as possible) must be taken into account.

Here are just some issues we would encounter in case of blind copy-pasting the Google's code:
  • As all we know, after the configuration changes your Activities (or non-retained Fragments) are recreated (I suppose you don't use this nasty hack android:configChanges="orientation|screenSize"). When they have been recreated the old ones must be garbage collected and if not - say hello to a memory leak. As will be discussed on the next paragraphs, the IntentService from the Google's sample during ongoing operation holds the indirect reference to the Activity associated with the ResultReceiver and hence prevents its deconstruction. Check also this StackOverflow question.
  • What if the IntentService sends the result at the time when the activity has been temporarily destroyed (e.g. after the device rotation)? Yes, it will go to nowhere and that means the whole work is simply lost. 
  • If the device quickly changes the location then each time it needs to obtain the new address data. But the IntentService doesn't provide us opportunity to cancel previous or pending tasks. So the device must waste resources for the operations which are completely useless now.
Ok, let's go ahead. I'll show you what I've found regarding this subject after some research.

How does IntentService send the results

To obtain the results from the IntentService Google suggests us to use a ResultReceiver subclass. The ResultReceiver implements Parcelable interface that's why it can be easily added to the Intent that is used to start the service. The following code snippet depicts this process:

1
2
3
4
5
6
7
8
        // Create an intent for passing to the intent service responsible for fetching the address.
        Intent intent = new Intent(this, FetchAddressIntentService.class);

        // Pass the result receiver as an extra to the service.
        intent.putExtra(Constants.RECEIVER, mResultReceiver);

        // Start the service
        startService(intent);

Every Android developer must be familiar with such procedure - it's a common task to start services or activities using intents containing some data. Usually we put data to the intent using Intent class putExtra method. For example, it can be some primitive type (int, boolean etc.) or an object that implements Parcelable interface. The data then can be extracted using such code:

mReceiver = intent.getParcelableExtra(Constants.RECEIVER);

Intuitively we expect to get a new copy of the parcelable object from the getParcelableExtra method. And it worth noting that this object should not influence the life-cycle of the original object we put in to the parcel. There are even answers on StackOverflow which suggest to use Parcelable interface as a deep copying method: link1, link2.

But it turns out this is not always the case. After a closer look at the Parcel docs I found a very interesting paragraph, here is the quote:
An unusual feature of Parcel is the ability to read and write active objects. For these objects the actual contents of the object is not written, rather a special token referencing the object is written. When reading the object back from the Parcel, you do not get a new instance of the object, but rather a handle that operates on the exact same object that was originally written. 
Wow! And how is that possible we do not get a new instance of the object? Does it mean we obtain just another reference to the original object and hence this reference will prevent it from being garbage collected? After a lot of searching, googling, stackoverflowing I simply didn't find any comprehensive answer.

As a last resort, I decided to look at the ResultReceiver.java source to find out what's really going on here. Of course, you can analyse it yourself, but here are the most important parts. According to the parcelable interface, we should implement writeToParcel and static CREATOR field which in turn requires a constructor taking a Parcel as an argument. So how does our ResultReceiver is getting unparceled? Look at the snippet:

1
2
3
4
5
    ResultReceiver(Parcel in) {
        mLocal = false;
        mHandler = null;
        mReceiver = IResultReceiver.Stub.asInterface(in.readStrongBinder());
    }

As you can see, the readStrongBinder() method is used to extract the original object from the parcel. And what the parcel docs state about it? Yay! That method deals with the aforementioned active objects which means we do not get a new instance!

To understand what is the IResultReceiver.Stub.asInterface line for look at the official docs regarding AIDL technology. Take a closer look at this statement:
  • Objects are reference counted across processes.
Finally I found the info I was looking for. It's easy to see now that our friend IntentService does not make a deep copy of the result receiver. Instead the rather complicated approach called AIDL is used here.

In simple words, what all this means is that as long as the IntentService is doing its job it will hold the indirect reference to the original result receiver object. And if the activity which started the service also points to the same object after the configuration change we have a memory leak. Nice.

Weak References and Retained Fragment

There are different methods you can find in the internet to overcome this issue. But most of them have disadvantages that are not suitable for the production code. For example, some guys suggest to save the ResultReceiver subclass into the Bundle and then extract it after the configuration change. But in this case only the base class ResultReceiver will be extracted and all our callbacks will be lost (callbacks are just interfaces, they cannot be added to a parcel!).

The only good advise I've found is to use LocalBroadcastManager and forget about this IntentService + ResultReceiver company.

Nonetheless, I finally managed to solve this problem using the good old weak references and a retained fragment. Here is the step-by-step instruction:
  • To allow the old activity garbage collection it must hold a weak reference to the result receiver object that's why the result receiver is stored in the retained fragment like this:

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    /**
     * A fragment that retains the stateful objects during a configuration change.
     */
    public final class MainRetainedFragment extends Fragment {
        /**
         * A key used by the fragment manager to set/retrieve the fragment.
         */
        public static final String FRAGMENT_TAG = "MAIN_RETAINED_FRAGMENT_TAG";
    
        /**
         * An object used to receive the result of the intent service work.
         */
        @Nullable
        private ResultReceiver mResultReceiver;
    
        /**
         * A factory method used to create a fragment in accordance with Android development best practices.
         * @return A new instance of MainRetainedFragment.
         */
        public static MainRetainedFragment newInstance() {
            return new MainRetainedFragment();
        }
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            // Retain this fragment.
            setRetainInstance(true);
        }
    
        @Override
        public String toString() {
            return "MainRetainedFragment{" +
                    "mResultReceiver=" + mResultReceiver +
                    '}';
        }
    
        /**
         * Gets the result receiver object.
         *
         * @return ResultReceiver object.
         */
        @Nullable
        public ResultReceiver getResultReceiver() {
            return mResultReceiver;
        }
    
        /**
         * Set the result receiver object.
         *
         * @param resultReceiver An instance of the ResultReceiver object.
         */
        public void setResultReceiver(@NonNull ResultReceiver resultReceiver) {
            mResultReceiver = resultReceiver;
        }
    }
    

  • Now the result receiver survives during the configuration changes and, moreover, it has a chance to cache the results obtained from the intent service and send them back to the activity when it is available again. The last code snippet depicts how we can access the result receiver object from the activity:

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            final FragmentManager fragmentManager = getSupportFragmentManager();
    
            MainRetainedFragment mainRetainedFragment = (MainRetainedFragment) fragmentManager.
                    findFragmentByTag(MainRetainedFragment.FRAGMENT_TAG);
    
            AddressResultReceiver addressResultReceiver;
    
            if (mainRetainedFragment == null) {
    
                // Create a new fragment if it is null (in case it is accessed for the first time).
                mainRetainedFragment = MainRetainedFragment.newInstance();
                fragmentManager.beginTransaction().
                        add(mainRetainedFragment, MainRetainedFragment.FRAGMENT_TAG).commit();
    
                // Create a new result receiver.
                addressResultReceiver = new AddressResultReceiver(new Handler());
    
                 // Save the result receiver in the retained fragment.
                mainRetainedFragment.setResultReceiver(addressResultReceiver);
    
                // Set the activity as a callback handler.
                // Note: this setter must set the callback handler as a weak reference.
                addressResultReceiver.setReceiverCallbackHandler(this);
            } else {
                // Use the retained result receiver.
                try {
                    addressResultReceiver = (AddressResultReceiver) mainRetainedFragment.getResultReceiver();
                } catch (ClassCastException e) {
                    e.printStackTrace();
                    throw new ClassCastException(" The mResultReceiver must be of AddressResultReceiver type.");
                }
            }
    
            // Set the result receiver as a weak reference.
            mResultReceiverWeakRef = new WeakReference<>(addressResultReceiver);
        }
    

I have tested this approach using LeakCanary - a memory leak detection library and everything seems to work fine.

As always, I hope this will help someone. If something is still unclear, please feel free to leave a comment.

No comments :

Post a Comment