Пользователь
0,0
рейтинг
19 апреля 2012 в 11:20

Разработка → Android UI thread

Большая часть кода Android приложения работает в контексте компонент, таких как Activity, Service, ContentProvider или BroadcastReceiver. Рассмотрим, как в системе Android организованно взаимодействие этих компонент с потоками.

При запуске приложения система выполняет ряд операций: создаёт процесс ОС с именем, совпадающим с наименованием пакета приложения, присваивает созданному процессу уникальный идентификатор пользователя, который по сути является именем пользователя в ОС Linux. Затем система запускает Dalvik VM где создаётся главный поток приложения, называемый также «поток пользовательского интерфейса (UI thread)». В этом потоке выполняются все четыре компонента Android приложения: Activity, Service, ContentProvider, BroadcastReceiver. Выполнение кода в потоке пользовательского интерфейса организованно посредством «цикла обработки событий» и очереди сообщений.

Рассмотрим взаимодействие системы Android с компонентами приложения.


Activity. Когда пользователь выбирает пункт меню или нажимает на экранную кнопку, система оформит это действие как сообщение (Message) и поместит его в очередь потока пользовательского интерфейса (UI thread).

Service. Исходя из наименования, многие ошибочно полагают, что служба (Service) работает в отдельном потоке (Thread). На самом деле, служба работает так же, как Activity в потоке пользовательского интерфейса. При запуске локальной службы командой startService, новое сообщение помещается в очередь основного потока, который выпонит код сервиса.

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

ContentProvider. Вызов локального ContentProvider немного отличается. ContentProvider также выполняется в основном потоке но его вызов является синхронным и для запуска кода ContentProvider не использует очередь сообщений.

Исходя из вышесказанного можно заметить, что если главный поток в данный момент обрабатывает пользовательский ввод или выполняет иное действие, выполнение кода, полученного в новом сообщении, начнётся только после завершения текущей операции. Если какая либо операция в одном из компонентов потребует значительного времени выполнения, пользователь столкнётся или с анимацией с рывками, или с неотзывчивыми элементами интерфейса или с сообщением системы «Приложение не отвечает» (ANR).

Для решения данной проблемы используется парадигма параллельного программирования. В Java для её реализации используется понятие потока выполнения (Thread).

Thread: поток, поток выполнения, иногда ещё упоминается как нить, можно рассматривать как отдельную задачу, в которой выполняется независимый набор инструкций. Если в вашей системе только один процессор то потоки выполняются поочередно (но быстрое переключение системы между ними создает впечатление параллельной или одновременной работы). На диаграмме показано приложение, которое имеет три потока выполнения:



Но, к сожалению, для взаимодействия с пользователем, от потока мало пользы. На самом деле, если вы внимательно посмотрите на диаграмму выше, вы поймёте, что как только поток выполнить все входящие в него инструкции он останавливается и перестаёт отслеживать действия пользователя. Чтобы избежать этого, нужно в наборе инструкций реализовать бесконечный цикл. Но возникает проблема как выполнить некое действие, например отобразить что-то на экране из другого потока, иными словами как вклиниться в бесконечный цикл. Для этого в Android можно использовать Android Message System. Она состоит из следующих частей:

Looper: который ещё иногда ещё называют «цикл обработки событий» используется для реализации бесконечного цикла который может получать задания используется. Класс Looper позволяет подготовить Thread для обработки повторяющихся действий. Такой Thread, как показано на рисунке ниже, часто называют Looper Thread. Главный поток Android на самом деле Looper Thread. Looper уникальны для каждого потока, это реализованно в виде шаблона проектирования TLS или Thread Local Storage (любопытные могут посмотреть на класс ThreadLocal в Java документации или Android).



Message: сообщение представляет собой контейнер для набора инструкций которые будут выполнены в другом потоке.

Handler: данный класс обеспечивает взаимодействие с Looper Thread. Именно с помощью Handler можно будет отправить Message с реализованным Runnable в Looper, которая будет выполнена (сразу или в заданное время) потоком с которым связан Handler. Код ниже иллюстрирует использование Handler. Этот код создаёт Activity которая завершиться через определённый период времени.

public class LaunchActivity extends Activity {
	// time to display the splash screen in ms
	private final static long SPLASH_DELAY = 4000 * 0;

	private final Handler handler = new Handler();
	private final Runnable splashTask = new Runnable() {
		@Override
		public void run() {
			finish();
		}
	};

	@Override
	protected void onResume() {
		super.onResume();
		handler.removeCallbacks(splashTask);
		handler.postDelayed(splashTask, SPLASH_DELAY);
	}

	@Override
	protected void onPause() {
		handler.removeCallbacks(splashTask);
		super.onPause();
	}
}


HandlerThread: написание кода потока реализующего Looper может оказаться не простой задачей, чтобы не повторять одни и те же ошибки система Android включает в себя класс HandlerThread. Вопреки названию этот класс не занимается связью Handler и Looper.

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

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

Подготовлено на основе материалов AndroidDevBlog
@minakov
карма
6,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (16)

  • –9
    пользователь сталкнётся
    От слова сталкер?
  • 0
    Помогите плиз по поводу взаимодействия с UI из thread
    На практике я сталкиваюсь с ситуацией, когда отдельный процесс, отображающий, например, прогрессдиалог в контексте вызвавшего его активити — падает с ошибкой при прерывании либо? закрытии основного активити.

    По видимому сценарий такой
    1. Пользователь нажал кнопку
    2. Стартовала задача с прогресс диалогом
    3. Пользователь переключился на другую программу либо еще что-то^^
    4. Асинхронная задача «потеряла» контекст и падает
    В результате я не могу отобразить например алерт диалог на потерю соединения либо закрыть корректно прогресс диалог, причем толком воспроизвести не удается, я вижу ошибки по логам багсенс, но не вижу причин

    Таск выглядит так:
    public class GetRequest extends AsyncTask<String, Integer, String> {

    private Activity activity;
    private Exception e=null;

    public GetRequest(Activity _activity) {
    this.activity = _activity;
    }


    @Override
    protected String doInBackground(String... params) {
    HttpGet getRequest = null;
    try {
    getRequest = new HttpGet(params[0]);
    Log.d("URL","url:"+params[0]);
    HttpResponse response = App.getClient().execute(getRequest);
    final int statusCode = response.getStatusLine().getStatusCode();
    Log.d("STATUS","status:"+Integer.toString(statusCode));
    if (statusCode != HttpStatus.SC_OK) {
    return "";
    }
    HttpEntity getResponseEntity = response.getEntity();
    String r = EntityUtils.toString(getResponseEntity);
    return r;
    }
    catch (Exception e) {
    if (null != getRequest) getRequest.abort();
    this.e=e;
    return "Error:"+e;
    }
    }

    @Override
    public void onPostExecute(String result) {
    if (null != e) {
    Log.e("GetRequest", "Exception", e);
    if (activity!=null) alert(e);
    //падает вот тут, проверка на нул не помогает
    //если закоментить строчку выше ошибка пропадет, но я теряю возвожность взаимодействия из асинтаска с уи
    }
    }

    private void alert(Throwable t) {
    AlertDialog.Builder builder=new AlertDialog.Builder(activity);

    builder
    .setTitle("Exception!")
    .setMessage(t.toString())
    .setPositiveButton("OK", null)
    .show();
    }
    }

    //запуск задачи
    try {
    result = new GetRequest(this).execute("http://something").get();
    } catch (Exception e) {
    e.printStackTrace();
    }
    • 0
      Причем я вижу, что в таких приложениях, как например андроид маркет — данная проблема по-видимому решена, так как при отсутствующем соединении вылетает алерт диалог типа соединение потеряно, но я не вижу пути как реализовать это безопасно кроме диких извратов навроде своего таскменеджера проверяющего не свалилась ли по эксепшену какая либо из запущенных задач + кэнселить все активные при событии при онпауз, типа if (mTaskManager!=null) mTaskManager.cancel(true); но это ж замучаешься столько кода каждый раз в каждом активити городить!
      • +1
        Создайте специальный Intent и подпишитесь на него в своём Activity, в GetRequest вместо создания диалога отправляйте Intent, а в Activity уже обработайте и выведите информацию
        • 0
          Да, это вариант, спасибо, но хочется получить макимально отчужденный от конкретных активити класс для бесполезного перетаскивания между проектами|активити с встроенной обработкой ошибок. Мне больше понравился подход предложенный ниже
  • +1
    Немного могу добавить.

    Мне Looper был интересен тем, что при том, что создает очередь сообщения, он еще вставляет poll метод (можно с натяжкой сказать sleep), т.е. отдает ресурсы системы, а не жрет всё процессорное время.

    При этом класс Looper достаточно простой и легко читаемый в исходниках, кажется там около 300 строк.
  • +3
    это проблема реализации класса AsyncTask в Android — неявная связь с инстансом текущей активити, а например при повороте экрана активити создаётся заново. один из инженеров гугл пишет что AsyncTask нужно использовать только для очень коротких тасков. решают эту проблему через явную привязку к активити code.google.com/p/texteasy/source/browse/trunk/src/org/texteasy/WeakAsyncTask.java
    • 0
      Большое спасибо, кажется это именно то, что я искал.

      Бегло просматривая сорцы соседних файлов заметил некую странность связанную с, по всей видимости, различными реализациями интерфейса для доступа к контакт листу в зависимоти от сдк?

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

      В моем приложении тоже считывается контакт лист, для осуществления звонков, отправки смс етк.
      Функция выглядит так:
        private ArrayList<Person> getContactList() {
          /*try {
            personList = new GetContacts(MActivity.this).execute().get();
          } catch (InterruptedException e) {
            e.printStackTrace();
          } catch (ExecutionException e) {
            e.printStackTrace();
          }*/
          if (startDialog == null) {
            startDialog = new ProgressDialog(MActivity.this);
            startDialog.setCancelable(true);
            startDialog.show();
            startDialog.setContentView(R.layout.emptydialog);
          }
          ArrayList<Person> contactList = new ArrayList<Person>();
          Cursor cur = null,phones = null;
          Person aContact = new Person();
          try {
            cur = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null); 
            if (cur.moveToFirst()) { 
              int idColumn = cur.getColumnIndex(ContactsContract.Contacts._ID); 
        
              int displayNameColumn = cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME); 
              do
                String contactId = cur.getString(idColumn); 
                String disPlayName = cur.getString(displayNameColumn); 
                //Например сыпется тут с ошибкой java.lang.IllegalStateException: Couldn't init cursor window
                int phoneCount = cur.getInt(cur 
                    .getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER));
               
                if(phoneCount>0){  
                  phones = getContentResolver().query( 
                      ContactsContract.CommonDataKinds.Phone.CONTENT_URI, 
                      null
                      ContactsContract.CommonDataKinds.Phone.CONTACT_ID 
                          + " = " + contactId, null, null); 
                  if(phones.moveToFirst()){ 
                    do
                      String phoneNumber= phones.getString(phones  
                          .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); 
                      aContact = new Person();
                      aContact.setPersonId(contactId);
                      aContact.setPhoneNum(phoneNumber);
                      aContact.setName(disPlayName.toLowerCase());
                      contactList.add(aContact);
                      //System.out.println(phoneNumber); 
                    }while(phones.moveToNext()); 
                  }
                }
            }while(cur.moveToNext());
           }
          }
          finally {
            if (cur != null) cur.close();
            if (phones != null) phones.close();
          }
          if (startDialog!=null) startDialog.dismiss();
          return contactList;
        }


      * This source code was highlighted with Source Code Highlighter.


      Я тестировал его на многих телефонах с различным андроид 2,3 и 4 — нигде ошибок не возникает. Однако на продакшене — мне постоянно сыпятся странные ошибки в логи. У приложения довольно 100000+ установок, в день в логах я вижу до десятка ошибок.
      То ли я где то накосячил, то ли есть какие то нюансы с этими грешными контактами. Перепробовал уже все. Почему трай кэтч не ловит — ума не приложу…
      • 0
        Я бы рекомендовал вместо AsyncTask управлять потоками в ручную и разрабатывать приложение так-же как и под другие платформы — в частности вручную контролировать все созданные потоки и при закрытии приложения, останавливать также и потоки. Если по каким-либо причинам остановить поток нету возможности (а такое бывает например если он висит на чтении из сокета — в Андроид есть связанный с этим баг), то освобождать все ссылки от потока к активити, переводя его в состояние «orphaned».
    • +1
      Пересоздание активити при повороте экрана можно отключить установив атрибут android:configChanges="keyboardHidden|orientation" элемента activity в файле манифеста. В этом случае интерфейс все таки будет переворачиваться, но со стороны контролов все будет выглядеть как будто просто изменились размеры.
      Странно почему не сделали такое поведение по умолчанию.
      • 0
        даже при залоченом (android:screenOrientation=«portrait») — возникает иногда
  • 0
    вот обвязка в активити для WeakAsyncTask

    public class TestActivity extends ListActivity {
    	/** State held between configuration changes. */
    	private State state;
    	private TestTask testTask;
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    
    		state = (State) getLastNonConfigurationInstance();
    		final boolean previousState = state != null;
    		if (previousState) {
    			testTask = state.testTask;
    			if (testTask != null) {
    				testTask.setTarget(this);
    			}
    		} else {
    			state = new State();
    			testTask = new TestTask(this);
    			state.testTask = testTask;
    		}
    	}
    
    	@Override
    	protected void onDestroy() {
    		if (testTask != null) {
    			testTask.cancel(true);
    		}
    		super.onDestroy();
    	}
    
    	@Override
    	public Object onRetainNonConfigurationInstance() {
    		state.testTask = testTask;
    		return state;
    	}
    
    	private void runTestTask() {
    		if (testTask != null && testTask.getStatus() == AsyncTask.Status.RUNNING) {
    			return;
    		}
    		if (testTask == null || testTask.getStatus() == AsyncTask.Status.FINISHED) {
    			testTask = new TestTask(this);
    		}
    		testTask.execute();
    	}
    
    	private static class TestTask extends WeakAsyncTask<Void, Void, Void, TestActivity> {
    
    		public TestTask(TestActivity target) {
    			super(target);
    		}
    
    		@Override
    		protected Void doInBackground(TestActivity target, Void... params) {
    			return null;
    		}
    
    		@Override
    		protected void onPostExecute(TestActivity target, Void result) {
    			super.onPostExecute(target, result);
    		}
    	}
    
    	private static class State {
    		TestTask testTask;
    	}
    
    }
    
    • 0
      Да, да, уже нашел, спасибо
  • –4
    Какова цель поста? Все есть в оф.блоге, документации и примерах…
    • 0
      Поставил минус, потому что не видел этого в документации, оф. блоге и примерах…

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.