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