Java Gym: Многопоточность в Android и Java, часть 3: Защита от ANR в Android.

Универсальные вещи — это хорошо, но и изобретать велосипеды не стоит. Android имеет особую проблему с потоками и несколько решений для нее. Эта проблема — ANR. Это выглядит вот так.

ANR, If you see it you will shit the bricks

Если вы видите это то вы (… кирпичи?), на самом деле вы делаете следующие движения в консоли.


$ adb pull '/data/anr/traces.txt'
$ less traces.txt

Внутри traces.txt мы видим что-то такое:


"android.server.ServerThread" prio=5 tid=8 NATIVE
| group="main" sCount=1 dsCount=0 s=N obj=0x45edae50 self=0x21e398
| sysTid=1101 nice=-2 sched=0/0 cgrp=default handle=2221272
at android.view.Surface.unlockCanvasAndPost(Native Method)
at android.view.ViewRoot.draw(ViewRoot.java:1431)
at android.view.ViewRoot.performTraversals(ViewRoot.java:1163)
at android.view.ViewRoot.handleMessage(ViewRoot.java:1727)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:123)
at com.android.server.ServerThread.run(SystemServer.java:528)

Здесь некая ViewRoot собралась перерисоваться, однако во время выполнения метода Surface.unlockCanvasAndPost(Native Method) у нее кончилось на это время. Возможно, что нажав «Wait» на окошке с предупреждением мы все исправим. Но ведь пользователь не читает глупых надписей, и такое приложение минимальный QA не пропустит.

Отсюда формулирую проблему многопоточности в Anroid: есть поток пользовательского интерфейса и в нём нельзя выполнять долгие операции, то есть по факту любые операции, про которые не известно, что это операции с O(1), то есть для неконстантных операций.

Здесь сразу нужно описать всю систему приложения для Android с графическим интерфейсом, в потоке которого нельзя вызывать долгие операции.

Итак такое приложение — это Activity. Если быть более честным, то Activity может быть в одном приложении много, но каждый такой компонент — потенциальный источник ANR.

Приведу наиболее простой подход к построению такого приложения.

  1. Создаём наследника класса Activity;
  2. Регистрируем его в AndroidManifest.xml, то есть добавляем туда
  3. Создаём в формате xml описание главного лайаута.
  4. В методе onCreate созданной Activity подключаем наш лайаут через setContentView().

При этом мы имеем доступ ко всем элементам представления через findViewById(). Проиллюстрируем весь процесс.

схема приложения в Android

Оперируя терминами MVC (Model-View-Controller — англ. Модель-Представление-Поведение): желтым на рисунке показаны элементы представления, а голубым — поведения. Модель — бизнес логика, которая может находится в других классах, организация которых — часть конкретного проекта. Наша же цель сейчас:

безопасно выполнить длительную операцию в каком-нибудь из Listener’ов,

отобразить текущее состояние модели по событию на View.

Первое, что видно сразу, нужно запустить в классе Listener’а поток, в котором будет выполнятся длинное действие, и поэтому самый сложный вопрос — это второй: как безопасно отобразить. Или как перейти в поток пользовательского интерфейса из выделенного потока задачи.

Первый прием — самый простой: «runOnUIThread()».

Для примера предположим, что у нас есть пользовательский интерфейс(допусим, он находится в main.xml), на котором есть два элемента: кнопка “увеличить счётчик” и TextView, на котором мы этот счётчик можем видеть. То есть, есть элемент строго ввода и строго вывода.


class MyActivity extends Activity {
private Button mIncButton = null; /* кнопка увеличения счётчика */
private TextView mCountView = null; /* вывод счетчика на экран */
private int mCnt = 0; /* собственно наш счётчик */
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.main);
mIncButton = (Button) findViewById(R.id.incbutton);
mCountView = (TextView) findViewById(R.id.cntview);
/* присоединяем поведение */
mIncButton.setOnClickListener(mIncClick);
/* инициализируем представление */
mCountView.setText(Integer.toString(mCnt));
}
/**
* Вот оно наше поведение, здесь мы будем выполнять инкремент
* счётчика как очень долгую операцию, в выделенном потоке.
*/
private final OnClickListener mIncClick = new OnClickListener () {
@Override
public void onClick(View v) {
/* поток для “очень длинной” операции */
Thread th = new Thread(new DoIncrement());
th.start();
}
};
/**
* Вот это собственно рабочий класс, который будет выполнять бизнес логику
* в выделенном потоке
*/
class DoIncrement implements Runnable {
public void run() {
/* гурбо и не безопасно, ибо нужна синхронизация и защита,
но для примера сойдет */
mCnt++;
/* теперь, то, ради чего пример писался */
UIThreadUtilities.runOnUIThread(MyActiviy.this, new UpdateUI());
}
}
/**
* Этот Runnable будет вызываться в потоке UI для обновления
* нашего представления.
*/
class UpdateUI implements Runnable {
public void run() {
/* хорошая практика делать так для состояний которых вообще не
может быть. В частности не может быть так, чтобы кэшируемое
представление не было скэшированно. */
assert(mCountView != null);
/* обновляем*/
mCountView.setText(Integer.toString(mCnt));
}
}
}

Прекрасно, и процетах в 50 случаев годится. Однако что делать если нужно передать много разных состояний и объектов в поток пользовательского интерфейса, да еще и не одним махом, да еще возможно и не из одного потока. Для этого случая в Android API есть класс Handler.

Handler.

Приведу вырожденный пример, в котором у нас будет поток, который непрерывно опрашивает модель на предмет некоторого сообщения, как только оно приходит он посылает эту строчку для обновления пользовательского интерфейса.


class MyOneMoreActivity extends Activity {

/**
* Метод, который обновляет пользовательский интерфейс
*/
private void updateLines(String message) {
/* какая разница как он его обновляет */
}
/**
* Опять таки метод, который обновляет UI, когда
* у нас прервалась связь с источником сообщений
*/
private void notifyEndOfSession() {
/* какая разница как */
}
/* доступные сообщения */
public static final int ADD_NEW_LINE = 23;
public static final int END_SESSION = 452;
/** звезда этого пример, все что в нем выполняется, выполняется
* в потоке пользовательского интерфейса
*/
private final Handler mHandler = new Handler () {
public void handleMessage(Message msg) {
switch (msg.what) {
case ADD_NEW_LINE:
assert(msg.obj instanceof String);
updateLines((String)msg.obj);
break;
case END_SESSION:
otifyEndOfSession();
break;
default:
/* о неизвестных сообщениях нужно узнать как можно раньше */
assert(false);
break;
}
}
};
class WorkingThread extends Thread {
@Override
protected void run() {
super.run();
/* получим нашу абстрактную модель, допустим одна синглетон */
AppModel model = AppModel.getInstance();
assert(model != null);
/* итерационный процесс, остановка при помощи interrupt() */
while (!isInterrupted()) {
String line = model.getLine();
if (line != null) {
/* посылаем полученную строчку, как сообщение */
Message msg = new Message();
msg.what = ADD_NEW_LINE;
msg.obj = line;
mHandler.sendMessage(msg);
}
/* не забываем давать возможность пожить и другим потокам */
yield();
}
/* все закончилось, и можно рассказать об этом потоку UI
sendEmptyMessage - тоже самое, что создать Message
и проинициализировать только what */
mHandler.sendEmptyMessage(END_SESSION);
}
}

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
/* здесь устанавливаем setContentView, кэшируем элементы
как в прошлом примере. Новое то, что мы еще запускаем поток модели */
new WorkingThread().start();
}
}

Кажется, что все проблемы решены, но и у этого решения есть слабые места. Во-первых плохо, если Activity имеет более одного Handler’а, а значит 5 — 7 сообщений и наш код уже плохо читаем. А 5 — 7 сообщений — это не 5 — 7 фичей, а значительно меньше, так как некоторые фичи нуждаются в 2 — 3 сообщениях. Для хорошей изоляции асинхронных задач в Android API есть класс AsyncTask.

AsyncTask или Thread и Handler в одном классе.

Больше всего этот класс походит для задач с визуализированным прогрессом, например загрузка контента и для задач, которые предусматривают вывод результатов работы на пользовательский интерфейс (почти всегда годное решение).

Покажем это на примере загрузчика файлов:


class FileLoaderActivity extends Activity {
/* индикатор прогресса, инициализируется в onCreate() */
private ProgressBar mProgress = null;

/* список из множества URL для загрузки, инициализируется в onCreate() */
privaet ListView mURLs = null;
private Adapter mAdapter = null;
/* для натуральности взял реальные URL c bash.org */
private static final String LABEL = “Label”;
private static final List<Map<String, Object>> mListOfURls = null;
static {
mListOfURls = List<Map<String, Object>>();
Map<String, Object> tmp = new HashMap<String, Object>();
tmp.put(LABEL, “http://bash.org.ru/comics/20111123”);
mListOfURls.add(tmp);
tmp = new HashMap<String, Object>();
tmp.put(LABEL, “http://bash.org.ru/comics/20111121”);
mListOfURls.add(tmp);
tmp = new HashMap<String, Object>();
tmp.put(LABEL, “http://bash.org.ru/comics/20111118”);
mListOfURls.add(tmp);
}
/** это то, что будет выполнятся, если мы кликнем по элементу списка */
private final OnItemClickListener mOnItemClick = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> a, View v, int pos, long time) {
/* получим выбранный элемент */
Map<String, Object> selected = (Map<String, Object>)mAdapter.getItem(pos);
if (selected != null) {
/* можно загрузить картинку */
LoadContent();
}
}
}
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.main);
mProgress = (ProgressBar) findViewById(R.id.progress);
mURLs = (ListView) findViewById(R.id.urls);
/* так как ListView - не тема этой заметки, то будем использовать все самое простое */
mAdapter = new SimpleAdapter(this,
mListOfURLs,
android.R.layout.activity_list_item,
new String[] {LABEL},
new int[] {android.R.id.text1});
mURLs.setAdapter(mAdapter);
mURLs.setOnItemClickListener(mOnItemClick);
}
/**
* Собственно это и есть класс ради которого мы тут всё это понаписывали
* Отмечу, что этот класс - generic, нечто вроде шаблона в С++. Передаваемые
* типы - типы аргументов для doInBackground, onProgressUpdate и onPostExecute.
* Собственно, как я выбирал типы:
* String - URL, для загрузки в бэкграунде
* Integer - хороший тип, чтобы передать процент загрузки
* Void - в нашем примере я ничего не буду делать с результатом. На то есть
* две причины: не хочу усложнять пример, хочу показать, что нужно написать
* когда не хочется ничего передавать.
* Важно отметить, что тип аргумента onPostExecute это тип возврата из
* doInBackground
*/
private class LoadContent extends AsyncTask<String, Integer, Void> {
@Override
protected void doInBackground(String... args) {
/* Для упрощения логики представим, что у на есть
класс Downloader, который принимает путь, есть
метод для получения размера файла, указания
размера буфера, и возможность считать буфер
(чудесный класс, сам по себе тема для статьи)*/
String url = args[0];
Downloader d = new Downloader(url);
d.setBufferSize(1024);
int size = d.getFileSize();
int downloaded = 0;
int readed = 0;
while ((readed = d.loadBuffer()) > 0) {
downloaded += readed;
/* теперь будет вызван onProgressUpdate */
publishProgress((downloaded / size) * 100);
}
/* все загрузили: теперь onPostExecute */
}
@Override
protected void onProgressUpdate(Integer... args) {
/* это поток пользовательского интерфейса
здесь можно обновить, например прогресс бар */
int progress = args[0];
mProgress.setProgress(progress);
}
/** вызывается перед началом таска, то есть перед doInBackground
*/
@Override
protected void onPreExecute() {
/* опять таки поток пользовательского интерфейса
здесь можно было бы например показать popup
"Идет загрузка ...", или как-то еще обозначить
что процесс пошел */
}
/** вызывается после завершения таска, то есть после doInBackground
*/
@Override
protected void onPostExecute(Void args) {
/* это поток пользовательского интерфейса, однако мы
договорились ничего не делать здесь */
}
}
}

И ещё одни приём обратной связи с View.

Итак, предположим что у нас есть много однотипных процессов догрузки ресурсов. На моей практике это была загрузка картинок в ListView. Тут правило такое: есть ресурс — можно перерисовать представление. Важная деталь: таких представлений много.

Каждое представление(наследник класса View) имеет два метода:

  • void post(Runnable runnable);
  • void postDelayed(Runnable runnable, long timeout);

Смысл прост: на прямую из выделенного потока мы с View ничего не можем, однако можем передать Runnable, который будет выполнен в цикле перерисовки самой View. Приведу сначала пример из официального учебника, потом покритикую его, и напишу свой. Итак официальный пример:


public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
final Bitmap b = loadImageFromNetwork();
mImageView.post(new Runnable() {
public void run() {
mImageView.setImageBitmap(b);
}
});
}
}).start();
}

Что в этом примере важно увидеть. Во-первых важно увидеть final перед Bitmap, так как если final убрать, то фокус не отработает, так как ссылка битмап к моменту выполнения переданного в post Runnabel уже будет потеряна.

В примере самая плохая вещь — это то, как он реализован. Важно, чтобы никто не думал, что это хорошая реализация. Попробую исправить, для начала именно это.


/* Начнём с конца. Вот этот класс будет обновлять View */
class UpdateView implements Runnable {
/*теперь ссылка не потеряется, и никаких final не нужно*/
private Bitmap mBitmap = null;
public UpdateView(Bitmap b) {
/* лучше заранее узнать, что где-то нам дают null-bitmap,
чем потом гадать, почему не перерисовывается */
assert(b != null);
/* сохраняем ссылку на битмап */
mBitmap = b;
}
@Override
public void run() {
mImageView.setImageBitmap(mBitmap);
}
}
/* собственно долгая задача */
class LoadBitmap implements Runnable {

@Override
public void run() {
Bitmap b = loadImageFromNetwork();
mImageView.post(new UpdateView(b));
}
}

Однако это всё ещё не очень хорошо. Во-первых мы имеем передаем ссылку на mImageView не самым лучшим образом. Фактически эта ссылка может быть изменена в процессе загрузки. Ну и ситуация здесь скорее требует runOnUIThread. Представим себе ситуацию с адаптером для GridView или ListView.


/**
* Класс которым будет оперировать адаптер
*/
class DataRecord {
/* эти два свойства - только для чтения, задаются при
создании экземпляра класса */
private String mTitle = null;
private String mURL = null;
/* служебные, присваиваются по необходимости */
private Bitmap mBitmap = null;
private View mView = null;
public DataRecord(String title, String url) {
mTitle = title;
mURL = url;
}
public String getTitle() {
return mTitle;
}
public String getURL() {
return mURL;
}
public Bitmap getBitmap() {
return mBitmap;
}
public void setBitmap(Bitmap b) {
mBitmap = b;
}
public View getView() {
return mView;
}
public void setView(View v) {
mView = v;
}
}
/**
* Это адаптер, который гипотетически может использоваться с GridView или ListView
*/
class MyAdapter extends ArrayAdapter<DataRecord> {
/* Джентльменский набор, из контекста и собственно данных.
в принципе можно получить эти данные и без этих ссылк, но для
короткого примера простительно */
private Context mContext = null;
private ArrayList<DataRecord> mData = null;
/* дефолтная картинка */
private Bitmap mDefault = null;
private Bitmap getDefualtImage() {
if (mDefault == null) {
mDefault = BitmapFactory.decodeResources(mContext.getResources(), R.drawable.defaultimage);
}
return mDefault;
}
/* Это конструктор, так надо <img src='http://idev.by/wordpress/wp-includes/images/smilies/icon_smile.gif' alt=':-)' class='wp-smiley' /> */
public MyAdapter(Context context, ArrayList<DataRecord> data) {
super(context, R.layout.row, data);
mContext = context;
mData = data;
}
/**
* Подход к написанию getView.
*/
public void getView(int pos, View view, ViewGroup parent) {
/* у ListView есть механизм повторного использования одних и техже
View, так что нам стоит проверить пришла нам старая или дали новую */
View v = view;
if (v == null) {
/* дали новую, то есть не дали и нужно сделать новую
раньше это делалось трохи иначе:
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.row, null);
Все же сейчас лучше делать, как показано ниже, а не выше
*/
v = getLayoutInflater().inflate(R.layout.row, null);
}
/* на чём нарисовать мы уже добыли, или получили, теперь нужно про
или пере инициализировать */
DataRecord d = mData.get(pos);
if (d != null) {
d.setView(v);
/* выставляем название */
TextView title = (TextView) v.findViewById(R.id.title);
title.setText(d.getTitle()):
/* выставляем картинку, но она большая */
if (d.getBitmap() == null) {
/* на тот случай если это переинициализация сразу
ставим дефолтную картинку*/
ImageView iv = (ImageView) v.findViewById(R.id.image);
iv.setImageBitmap(getDefualtImage());
/* сохраним дефолтную картинку и тут, чтобы повторно не
вызывать загрузку когда мы окажемся здесь в
следующий раз. */
d.setBitmap(getDefualtImage());
/* и тутже грузим настоящую */
new Thread(new LoadTask(d)).start();
}
}
return v;
}
/** Процесс обновления View, будет запускаться в потоке пользовательского
* интерфейса.
*/
class UpdateTask implements Runnable {
private DataRecord mData = null;
public LoadTask(DataRecord d) {
mData = d;
}
@Override
public void run() {
assert(mData != null);
View v = mData.getView();
assert(v != null);
ImageView iv = (ImageView) v.findViewById(R.id.image);
assert(iv != null);
Bitmap b = mData.getBitmap();
assert(b != null);
iv.setImageBitmap(b);
}
}
/**
* Поток загрузки, а точнее Runnable, который будет выполнятся в
* потоке загрузке.
*/
class LoadTask implements Runnable {

private DataRecord mData = null;
public LoadTask(DataRecord d) {
mData = d;
}
@Override
public void run() {
assert(mData != null);
/* я использую для загрузки туже мифическую процедуру из
учебного примера, да простят меня читатели. */
mData.setBitmap(loadImageFromNetwork());
assert(mData.getView != null);
mData.getView.post(new UpdateTask(mData));
}
}
}

Достоинства показанного решения состоят в том, что потоков загрузки может быть достаточно много, но не более одного на картинку.

Вот пожалуй и все основные технические решения для защиты вашего Android приложения от ANR.

Материал предоставлен компанией Softeq Development, FLLC

Java Gym: Многопоточность в Android и Java, часть 3: Защита от ANR в Android.

Рейтинг
( Пока оценок нет )
webnewsite.ru / автор статьи
Загрузка ...

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: