StrictMode и борьба с ANR
Начиная с версии Gingerbread, Google добавил в Android механизм, который позволяет отслеживать долгосрочные операции, выполняемые в UI потоке. Имя этому механизму StrictMode. Для чего же это было сделано?
Каждый из вас, вероятно, сталкивался в приложениях с диалоговым окном «Application Not Responding» (приложение не отвечает) или ANR. Это происходит когда приложение не отвечает на события ввода (нажатие клавиш, тач по экрану) в течение 5 секунд. Для отслеживания процессов, блокирующих UI поток, и был введен механизм StrictMode. И, начиная с Android 3.0, StrictMode пресекает попытки выполнить сетевой запрос в UI потоке.
Итак, какие же механизмы предоставляет Android для обращения к сети вне UI потока?
AsyncTask
Наверное, одним из самых распространенных механизмов, помогающих выполнять сетевые операции, является AsyncTask. Рассмотрим на примере:

На первый взгляд все нормально, но есть несколько минусов:
- При смене ориентации девайса, сетевой поток (Worker Thread) потеряет контекст Activity в рамках которой он был запущен. И при получении результата — IllegalStateException. Обычно, для решения этой проблемы, отменяют сетевой поток в методе Activity.onStop (как вариант в onPause) и запускают снова в Activity.onStart (onResume). Но при таком подходе пользователь может попросту не дождаться результата, не говоря об увеличении траффика.
- Необходимость сохранять результат запроса между сменой конфигураций.
- Если приложение находится в фоне, система может его завершить вместе со всеми его потоками.
Начиная с 3.0, нам на выручку приходит механизм, который позволяет решить большинство описанных выше проблем. Но что делать, если требования к платформе ниже 3.0? Не отчаиваться и использовать support-library в которую этот механизм был бэкпортирован.
Loaders
Чем же так хорош этот механизм:
- Доступен из Activity и Fragment
- Отслеживает текущее состояние приложения (видимо пользователю, находится в фоне и т.д.)
- Предоставляет возможность асинхронной загрузки данных
- Контролирует источник данных
- Автоматически возвращает результат последнего запроса при смене конфигурации. Таким образом, нет необходимости повторного запроса
Давайте рассмотрим как можно с помощью Loader'ов запросить данные по сети:

Как это работает? Внутри Loader'а запускается сетевой поток, полученный результат парсится и отдается в ContentProvider, который сохраняет их в DataStorage (это может быть память, файловая система, sqlite база данных) и оповещает Loader о том, что данные были изменены. Loader, в свою очередь, опрашивает ContentProvider на предмет новых данных и возвращает их в Activity (Fragment). Если заменить сетевой поток сервисом, мы сможем гарантировать что пользователь получит данные даже в том случае, если приложение было свернуто (так как у Service приоритет выше чем у background процесса).
В чем преимущество данного подхода:
- Возможность реализовать кэширование запросов
- Прозрачный сетевой слой
- Возможность легко исключить сетевой слой и получить offline-приложение.
- Независимость от формата возвращаемых данных, нам важны непосредственно данные, которые необходимо визуализировать.
- Одна точка входа для обращения к данным
Пример реализации
public abstract class AbstractRestLoader extends Loader<Cursor> { private static final int CORE_POOL_SIZE = Android.getCpuNumCores() * 4; private static final int MAX_POOL_SIZE = CORE_POOL_SIZE * 4; private static final int POOL_KEEP_ALIVE = 1; private static final BlockingQueue<Runnable> sPoolWorkQueue; private static final ThreadFactory sThreadFactory; private static final ExecutorService sThreadPoolExecutor; private static final AsyncHttpClient sDefaultHttpClient; private static final Handler sUiHandler; static { sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(CORE_POOL_SIZE * 2); sThreadFactory = new LoaderThreadFactory(); sThreadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, POOL_KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory ); sDefaultHttpClient = new AsyncHttpClient(); sUiHandler = new Handler(Looper.getMainLooper()); } private final AsyncHttpClient mHttpClient; private final HttpMethod mRestMethod; private final Uri mContentUri; private final ContentObserver mObserver; private String[] mProjection; private String mWhere; private String[] mWhereArgs; private String mSortOrder; private boolean mLoadBeforeRequest; private FutureTask<?> mLoaderTask; private AsyncHttpRequest mRequest; private Cursor mCursor; private boolean mContentChanged; public AbstractRestLoader(Context context, HttpMethod request, Uri contentUri) { super(context); mHttpClient = onInitHttpClient(); mRestMethod = request; mContentUri = contentUri; mObserver = new CursorObserver(sUiHandler); } public Uri getContentUri() { return mContentUri; } public AbstractRestLoader setProjection(String[] projection) { mProjection = projection; return this; } public AbstractRestLoader setWhere(String where, String[] whereArgs) { mWhere = where; mWhereArgs = whereArgs; return this; } public AbstractRestLoader setSortOrder(String sortOrder) { mSortOrder = sortOrder; return this; } public AbstractRestLoader setLoadBeforeRequest(boolean load) { mLoadBeforeRequest = load; return this; } @Override public void deliverResult(Cursor cursor) { final Cursor oldCursor = mCursor; mCursor = cursor; if (mCursor != null) { mCursor.registerContentObserver(mObserver); } if (isStarted()) { super.deliverResult(cursor); mContentChanged = false; } if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { oldCursor.unregisterContentObserver(mObserver); oldCursor.close(); } } @Override protected void onStartLoading() { if (mCursor == null || mContentChanged) { forceLoad(); } else { deliverResult(mCursor); } } @Override protected void onForceLoad() { cancelLoadInternal(); if (mLoadBeforeRequest) { reloadCursorInternal(); } restartRequestInternal(); } @Override protected void onReset() { cancelLoadInternal(); if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } protected AsyncHttpClient onInitHttpClient() { return sDefaultHttpClient; } protected void onCancelLoad() { } protected void onException(Exception e) { } protected void deliverResultBackground(final Cursor cursor) { sUiHandler.post(new Runnable() { @Override public void run() { deliverResult(cursor); } }); } protected void deliverExceptionBackground(final Exception e) { sUiHandler.post(new Runnable() { @Override public void run() { onException(e); } }); } protected abstract void onParseInBackground(HttpHead head, InputStream is); protected Cursor onLoadInBackground(Uri contentUri, String[] projection, String where, String[] whereArgs, String sortOrder) { return getContext().getContentResolver().query(contentUri, projection, where, whereArgs, sortOrder); } private void reloadCursorInternal() { if (mLoaderTask != null) { mLoaderTask.cancel(true); } mLoaderTask = new FutureTask<Void>(new Callable<Void>() { @Override public Void call() throws Exception { deliverResultBackground(onLoadInBackground(mContentUri, mProjection, mWhere, mWhereArgs, mSortOrder)); return null; } }); sThreadPoolExecutor.execute(mLoaderTask); } private void restartRequestInternal() { if (mRequest != null) { mRequest.cancel(); } mRequest = mHttpClient.execute(mRestMethod, new AsyncHttpCallback() { @Override public void onSuccess(HttpHead head, InputStream is) { onParseInBackground(head, is); } @Override public void onException(URI uri, Exception e) { deliverExceptionBackground(e); } }); } private void cancelLoadInternal() { onCancelLoad(); if (mLoaderTask != null) { mLoaderTask.cancel(true); mLoaderTask = null; } if (mRequest != null) { mRequest.cancel(); mRequest = null; } } private static final class LoaderThreadFactory implements ThreadFactory { private final AtomicLong mId = new AtomicLong(1); @Override public Thread newThread(Runnable r) { final Thread thread = new Thread(r); thread.setName("LoaderThread #" + mId.getAndIncrement()); return thread; } } private final class CursorObserver extends ContentObserver { public CursorObserver(Handler handler) { super(handler); } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { onChange(selfChange, null); } @Override public void onChange(boolean selfChange, Uri uri) { if (isStarted()) { reloadCursorInternal(); } else { mContentChanged = true; } } } }
Пример использования
public class MessageActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor> { private ListView mListView; private CursorAdapter mListAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ac_message_list); mListView = (ListView) findViewById(android.R.id.list); mListAdapter = new CursorAdapterImpl(getApplicationContext()); getSupportLoaderManager().initLoader(R.id.message_loader, null, this); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new AbstractRestLoader(getApplicationContext(), new HttpGet("API URL"), null) { @Override protected void onParseInBackground(HttpHead head, InputStream is) { try { getContext().getContentResolver().insert( Messages.BASE_URI, new MessageParser().parse(IOUtils.toString(is)) ); } catch (IOException e) { deliverExceptionBackground(e); } } @Override protected void onException(Exception e) { Logger.error(e); } }.setLoadBeforeRequest(true); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mListAdapter.swapCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { mListAdapter.swapCursor(null); } }
P.S.
Developing Android REST client applications
Android Developers — Loaders
Android Developers — Processes and Threads
