Pull to refresh

Эффективное клиент-серверное взаимодействие в Android

Reading time6 min
Views5.9K

StrictMode и борьба с ANR


Начиная с версии Gingerbread, Google добавил в Android механизм, который позволяет отслеживать долгосрочные операции, выполняемые в UI потоке. Имя этому механизму StrictMode. Для чего же это было сделано?

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

Итак, какие же механизмы предоставляет Android для обращения к сети вне UI потока?

AsyncTask


Наверное, одним из самых распространенных механизмов, помогающих выполнять сетевые операции, является AsyncTask. Рассмотрим на примере:
image
На первый взгляд все нормально, но есть несколько минусов:
  • При смене ориентации девайса, сетевой поток (Worker Thread) потеряет контекст Activity в рамках которой он был запущен. И при получении результата — IllegalStateException. Обычно, для решения этой проблемы, отменяют сетевой поток в методе Activity.onStop (как вариант в onPause) и запускают снова в Activity.onStart (onResume). Но при таком подходе пользователь может попросту не дождаться результата, не говоря об увеличении траффика.
  • Необходимость сохранять результат запроса между сменой конфигураций.
  • Если приложение находится в фоне, система может его завершить вместе со всеми его потоками.

Начиная с 3.0, нам на выручку приходит механизм, который позволяет решить большинство описанных выше проблем. Но что делать, если требования к платформе ниже 3.0? Не отчаиваться и использовать support-library в которую этот механизм был бэкпортирован.

Loaders


Чем же так хорош этот механизм:
  • Доступен из Activity и Fragment
  • Отслеживает текущее состояние приложения (видимо пользователю, находится в фоне и т.д.)
  • Предоставляет возможность асинхронной загрузки данных
  • Контролирует источник данных
  • Автоматически возвращает результат последнего запроса при смене конфигурации. Таким образом, нет необходимости повторного запроса

Давайте рассмотрим как можно с помощью Loader'ов запросить данные по сети:
image
Как это работает? Внутри 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
Tags:
Hubs:
Total votes 9: ↑6 and ↓3+3
Comments9

Articles