Watch out for JobIntentService memory leak

Michał Łuszczuk
4 min readAug 27, 2018

--

To introduce you to this very bad JobIntentService memory leak I must start from the beginning.

Fun fact to remember:

PowerManager manager = (PowerManager) activityReference.getSystemService(POWER_SERVICE);
  1. This code above creates PowerManager service.
  2. PowerManager instance created using getSystemService method of activity internally has reference to this Activity:
Power manager with field/reference to MainActivity

Now: Let say we want to upload or make some time consuming calculations in the background.

IntentService or newer Oreo compatibile JobIntentService class from support library looks like perfect tool to implement this task.

At the end we finish with this implementation:

public class CalculationsJobIntentService extends JobIntentService {

private static final int RANDOM_JOB_ID = 1302;

public static void enqueueWork(Context context, Intent work) {
enqueueWork(context,
CalculationsJobIntentService.class,
RANDOM_JOB_ID,
work);
}

@Override
protected void onHandleWork(@NonNull Intent intent) {
// some time consuming calculations
}
}

And now of course we want to start this job from our MainActivity , with code:

public class MainActivity extends AppCompatActivity {

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

CalculationsJobIntentService.enqueueWork(this, new Intent());
// some other activity code;
}
}

Everything easy and clear. MainActivity starts and our JobIntentService is started also.

What would you think if I told you that this code will leak your MainActivity object instance forever?

To prove how this memory leak comes to life, we need to go deeper. Deep to the JobIntentService class implementation.

Every JobIntentService is started using enqueueWork method, which needs object of Context type as a first parameter:

Part of JobIntentService source code (enqueueWork) method

Later, next enqueueWork method is called:

Part of JobIntentService source code (enqueueWork 2) method

As we can see, later internally getWorkEnqueuer method is called with implementation:

This method base on Android version creates JobWorkEnqeuer class, or CompatWorkEnqueuer class.

Very important fact to notice:
single (specific for every service type) JobIntentService.WorkEnqueuer is created only once (because later it is stored in sClassWorkEnqueuer map).

And this map is static field of JobIntentService class

To be clear, there are only put and get methods used to interact with this map in whole JobIntentService source code (no remove, clear etc.). So once any WorkEnqueuer is added to this map it stays there forever.

But we can say: As long as CompatWorkEnqueuer or JobWorkEnqueuer do not have any references to the Context they could stay there forever and everything still will be fine.

So now look at CompatWorkEnqueuer implementation:

It uses context (in our implementation this is MainActivity object instance) to get PowerManager service.
Quick back to the beginning — PowerManager created with MainActivity as a context will hold reference to MainActivity.

But wait, PowerManager is only local variable, so how this could make any activity memory leakage?

The problem is in WakeLocks!

WakeLock class is inner class of PowerManager class. Inner class (not static nested class) always holds reference to outer class, in this case any created WakeLock will hold PowerManager object reference. This is it!

So how this memory leak realy looks like:

  1. JobIntentService.sClassWorkEnqueuer — static map which holds all created WorkEnqueuers (in this case CompatWorkEnqueuer)
  2. CompatWorkEnqueuer — holds reference to two wake locks
  3. WakeLock — holds reference to PowerManager
  4. PowerManager — holds reference to Context — in our case MainActivity

So for every Android with api under 25, starting JobIntentService with Activity context will leak this context (Activity) once.

If you still want to use JobIntentService, please remember to pass activity.getApplicationContext() — then static map indirectly will hold only Application object instance reference (which is not such a big deal)

--

--