2

I have a few fragments in my activity. Each fragment makes a call to an AsyncTask which loads data from the local DB in the onCreate() method like this:

@Override
public void onCreate(Bundle savedInstance) {
    super.onCreate(savedInstance);
    (new DataLoaderTask()).execute();
    ...
}

I have three fragments that do this. In doing so, the fragments do not finish drawing their UI until the DataLoaderTask completes. Why is this? I tried changing the call to this and I no longer have the issue:

@Override
public void onCreate(Bundle savedInstance) {
    super.onCreate(savedInstance);
    (new Handler()).postDelayed(new Runnable() {
        @Override
        public void run() {
            (new DataLoaderTask()).execute();
        }
    }
    ...
}

Why does making the call inside a Runnable passed into a Handler work? Shouldn't the AsyncTask be running in the background anyway and hence the UI should get drawn before it completes?

Thanks for your help.

UPDATE: Adding more info. Here's the constructor of DataLoaderTask():

public DataLoaderTask(Object object) {
    mObject = object;
    mListeners = new ArrayList<OnUpdateListener>();
}

DataLoaderTask does NOT override the onPreExecute() method. It does override the onPostExecute() method. I've timed my onPostExecute() method and it takes approx ~2ms. All it's doing is updating some objects and calling a method on any Listeners provided.

UPDATE: Here's the full onPostExecute() method:

@Override
protected void onPostExecute(Object result) {
    synchronized(mDataManagerLock) {
        if (Config.isLogging()) {
            Log.i(TAG, "*** *** *** *** Finished syncing with database (onPostExecute()).");
        }
        if (mObject instanceof Playlist && result instanceof Playlist) {
            if (((Playlist) result).isMyPlaylist()) {
                synchronized(mTmpMyPlaylist) {
                    if (mTmpMyPlaylist != null && !mTmpMyPlaylist.isEmpty()) {
                        ((Playlist) result).addPlaylistActions(mTmpMyPlaylist.getPlaylistActions());
                        mTmpMyPlaylist.clear();
                    }
                }
            }
            if (mergeLocalPlaylistChanges((Playlist) result) && Config.isLogging()) {
                Log.i(TAG, "Local playlist changes during sync merged.");
            } else if (Config.isLogging()) {
                Log.i(TAG, "No local playlist changes were made during sync.");
            }
            ((Playlist) mObject).replace((Playlist) result);
            putPlaylist((Playlist) mObject, null /*newClips*/);
        } else if (mObject instanceof Catalog && result instanceof Catalog) {
            ((Catalog) mObject).replace((Catalog) result);
            putCatalog((Catalog) mObject, null);
        } else if (mObject instanceof NotificationsList) {
            // We've synced NotificationsList in Memory with Disk.
            mNotificationsList.setLastNetworkFetchTime(mApp.getNotificationsNetworkFetchTime());
        } else if (mObject instanceof SetsList) {
            // We've synced SetsList in Memory with Disk.
            mSetsList.setLastNetworkFetchTime(mApp.getExploreNetworkFetchTime());
        }
        if (Config.isLogging()) {
            Log.i(TAG, mObject.getClass().getSimpleName() + " after sync with database: " + mObject);
        }
    }
    if (Config.isLogging()) {
        Log.i(TAG, "Finished syncing in memory object/list with database.\n *** Time taken: " + (System.currentTimeMillis() - mStartTime)/1000 + " milliseconds.");
    }
    if (mListeners != null) {
        for (OnUpdateListener listener : mListeners) {
        if (listener != null) {
            listener.onUpdated();
        }
    }
}
}
b.lyte
  • 6,518
  • 4
  • 40
  • 51
  • Post your `DataLoaderTask`'s constructor, if applicable. What are you doing in `onPreExecute()`? – 323go Jul 23 '14 at 23:49
  • Show your `DataLoaderTask`. Perhaps you're doing expensive operations in methods that run on the ui thread. – fabian Jul 23 '14 at 23:53
  • I've updated the question. Maybe I should provide the actual onPostExecute() method. However, like I mentioned, the onPostExecute() method doesn't take long to execute. – b.lyte Jul 23 '14 at 23:56
  • Where else are you synchronizing on mDataManagerLock? – alanv Jul 24 '14 at 00:23
  • @alanv I'm synchronizing mDataManagerLock in several other method calls as well as in `doInBackground()` – b.lyte Jul 24 '14 at 00:25
  • @alanv none of the method calls are called during startup however. So the only other time the `synchronized(mDataManagerLock)` is encountered is during `doInBackground()`. – b.lyte Jul 24 '14 at 00:35
  • So I was wrong, onPostExecute() takes about 40 - 90 ms. That said, it should still not cause the UI to take so long to draw. The UI is still waiting until after the DataLoaderTask completes before it draws, which should not be happening correct? – b.lyte Jul 24 '14 at 01:10

1 Answers1

0

As you mentioned, anything resource intensive or that is going to take some time should be done in doInBackground. This includes not running anything that might hold it off in onPostExecute since we know that it runs in the main thread (with some exceptions) and might cause unexpected behavior like this.

I've ran some tests, apparently the main thread is being hold by onPostExecute. I created a dummy Task:

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

    @Override
    protected Void doInBackground(Void... params) {
        return null;
    }

    @Override
    protected void onPostExecute(Void result) {
        try {
            Log.i(TAG, "I'm holding the UI back.");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        super.onPostExecute(result);
    }

}

Even if you post to a handler it takes the task and holds it. It wasn't until I added it to a Thread that the UI was printing on time:

@Override
protected void onPostExecute(Void result) {
    new Thread(new Runnable() {

        @Override
        public void run() {
            try {
                Log.i(TAG, "I'm NOT holding the UI back.");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    super.onPostExecute(result);
}

Moral of the story, DO NOT do anything that might hold the main thread. And I am not saying you should create a Thread in onPostExecute either, that is beyond a bad practice if you are not somehow managing that thread.

Toguard
  • 437
  • 3
  • 9
  • This didn't work either. It's still waiting for AsyncTask to complete for some reason. I'm going to instead try waiting for the UI to draw (using GlobalLayoutListener) and execute AsyncTask then. Will post full solution if it works! – b.lyte Jul 24 '14 at 16:55
  • So what exactly did you do? All I explained is that onPostExecute is holding off the UI, which is what you are facing, you must run everything resource intensive in doInBackground. Also, note that if you are waiting for some kind of UI update from something after the processing, it's obvious it won't show up. – Toguard Jul 24 '14 at 18:35
  • So my `onPostExecute()` takes less than 90ms to execute. I know that `onPostExecute()` runs on the UI thread, however, the issue is that my UI should draw long before `doInBackground()` and hence `onPostExecute()` is run. Instead, what I've done is add a `OnGlobalLayoutListener`. Within the `onGlobalLayout()` callback I execute the AsyncTask. By doing this, I ensure that the UI is drawn and does not wait for the AsyncTask. This is a workaround of course. When I get a chance I need to figure out why the UI is waiting for the AsyncTask to complete if it's called in `onCreate()`. – b.lyte Jul 24 '14 at 19:10
  • This question explains how to create a GlobalLayoutListener: http://stackoverflow.com/questions/7733813/how-can-you-tell-when-a-layout-has-been-drawn – b.lyte Jul 24 '14 at 19:13
  • Well the question was a reason of why the UI was not drawing. Here onPostExecute is somehow getting ahold of the main thread, it's making it wait. Also, remember that onCreate is run prior to drawing anything, the queue might be allowing the task to run before moving on to the next phase. (Just confirmed, onStart and onResume are executing) – Toguard Jul 24 '14 at 19:36
  • Hmm, so maybe the AsyncTask is executing quickly enough that its onPostExecute grabs the main thread before the activity/fragment begin drawing? In that case is it best practice to only call execute on an AsyncTask after the UI is drawn? I guess my question essentially leads to a design question of best practices. Should we be calling execute() on an AsyncTask in the onCreate() method at all? Or is this risky? – b.lyte Jul 24 '14 at 19:41
  • I'm trying to work towards a startup time like many of Googles own apps. For example, launch Gmail, Google Calendar, Google Hangouts and you'll see that the action bar immediately gets drawn. That is, the actionbar is drawn so quickly that it appears when the app zooms into view (or whatever startup animation it has). How can one achieve this? – b.lyte Jul 24 '14 at 19:43