5

I need to do some background work which requires a Context in a JobService (I'm using the Firebase JobDispatcher one because we support api 16+) I've read a lot of articles about the JobService and AsyncTasks but I'm unable to find any good articles on how to combine them if you need a Context.

My JobService

import com.firebase.jobdispatcher.JobParameters;
import com.firebase.jobdispatcher.JobService;

public class AsyncJobService extends JobService {

    @Override
    public boolean onStartJob(JobParameters job) {
        new AsyncWork(this, job).execute();
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters job) {
        return false;
    }
}

My AsyncTask

import android.os.AsyncTask;
import com.firebase.jobdispatcher.JobParameters;
import com.firebase.jobdispatcher.JobService;

class AsyncWork extends AsyncTask<Void, Void, Void> {

    private JobService jobService;

    private JobParameters job;

    AsyncWork(JobService jobService, JobParameters job) {
        this.jobService = jobService;
        this.job = job;
    }

    @Override
    protected Void doInBackground(Void... voids) {
        // some work that needs context
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        // some work that needs context
        jobService.jobFinished(job, false);
    }
}

This gives a warning that the jobService property in the AsyncWork class is leaking a context object. I understand why this is the case if you pass an Activity or Fragment but this is a JobService which should exist untill I call jobFinished(). Am I doing something wrong or can I ignore the warning?

Sander
  • 808
  • 6
  • 18
  • 1
    Why are you using `AsyncTask` in the first place? The point behind `AsyncTask` is to do work on the main application thread after the background work is completed. There should be no reason for a `JobService` to want to do anything on the main application thread. So, use a regular `Thread`, or some form of `Executor`. – CommonsWare Aug 30 '17 at 16:05
  • The JobService is being used to download data and images for our appwidget and update the appwidget. I've always assumed that updating an AppWidget with new RemoteViews needs to be done on the UiThread for some reason. It seems like my assumption was incorrect and I indeed do not need an AsyncTask. I'm going to try a regular `Thread` tomorrow. Thanks! – Sander Aug 30 '17 at 16:34
  • 1
    Nope, you can update an app widget via `AppWidgetManager` from a background thread. You will need a `Context`, but that can be the `Application` singleton, so as not to introduce any memory leaks. `AsyncTask` is "on the outs" in general, but it was only ever intended for use within primary UI (activities, fragments). – CommonsWare Aug 30 '17 at 16:38
  • It all makes a lot more sense now. The "View" part of the name `RemoteViews` has always tricked me into preparing the `RemoteViews` on a worker thread and updating it through the `AppWidgetManager` on the UiThread. I only realized that this was a mistake because you questioned my use of an `AsyncTask`. I will use the suggestions you've given. Thanks again! – Sander Aug 30 '17 at 17:19

2 Answers2

1

You cannot ignore the warning. Because AsyncWork holds the reference to the Context, the Context cannot be GCed until the task is complete: the Context memory leaks. There are two solutions:

  1. Use a context that is long-lived, anyway, the Application context.
  2. Tie the life of the async task to the life of the context to which it hold a reference: cancel it in onPause
G. Blake Meike
  • 6,615
  • 3
  • 24
  • 40
  • 2
    1. I can't use the application context because I need to call the jobFinished() function in onPostExecuted() to release the wakelock. 2. To the best of my knowledge the asynctask is tied to the life of the JobService because I'm returning true in onStartJob() of the JobService and calling jobFinished() at the end of onPostExecute(). There is no onPause() in a JobService, would calling cancel() in onStopJob() prevent the leak 100% of the time even though the warning remains? – Sander Aug 30 '17 at 15:42
  • What "leak" means, in this case, is that the Android framework needs the memory used by a now-obsolete `Activity` object but cannot get it because `AsyncWork` is holding a reference. I don't understand the details of your use case but that is what you need to prevent. You also need to be aware that once `onDestroy` is called, the `Activity` that you passed as context is no longer in a consistent state. – G. Blake Meike Aug 30 '17 at 17:21
  • I note that `AsyncWork` does not return anything. Have you considered putting the whole mess into an `IntentService`? – G. Blake Meike Aug 30 '17 at 17:23
  • I'm sorry if my question is unclear. I understand how `Context` leaks work from `Activity` and `Fragment`, my question is related to how it works when the `AsyncTask` is called from a `JobService` which has a completely different lifecycle. @CommonsWare pointed out in the comments on the question that I shouldn't need an `AsyncTask` in the first place and that's what the real problem was. The issue is resolved now, thanks a lot for trying to help! – Sander Aug 30 '17 at 17:46
  • Whatevs. I totally defer to @CommonsWare . An `AsyncTask`, however, is, exactly, the use of an `Executor`. If you put your task on a home-made `Executor`, and hold a reference to the `Activity` you have the exact same problem. ... and I *did* suggest an `IntentService` – G. Blake Meike Aug 30 '17 at 18:43
0

To handle leaking you need to use WeakRefernce class with JobService Object

class AsyncWork extends AsyncTask<Void, Void, Void> {

  private WeakReference<JobService> jobServiceWeakReference;
  private JobParameters job;

  AsyncWork(JobService jobService, JobParameters job) {
      this.jobServiceWeakReference = new WeakReference<>(jobService);
      this.job = job;
  }

  @Override
  protected Void doInBackground(Void... voids) {
      // some work that needs context
        return null;
    }

  @Override
  protected void onPostExecute(Void aVoid) {
      super.onPostExecute(aVoid);
      // some work that needs context
      jobServiceWeakReference.get().jobFinished(job, false);
  }
}
AmrDeveloper
  • 3,826
  • 1
  • 21
  • 30