При создании мобильного приложения чуть сложнее «Hello, world» почти наверняка требуется скачать что-то из Сети или считать файл с диска. Для стабильной работы программы в целом эти действия должны совершаться в отдельных потоках. Зачем, когда и как генерировать новые потоки в Android — об этом ты узнаешь в этой статье.

 

Процессы и потоки

Прежде чем разбираться с Android API, вспомним, какой структурой обладает эта ОС. В ее основе лежит Linux-ядро, в котором реализованы базовые механизмы, присущие всем *nix-системам. В ядре собраны модули, предназначенные для низкоуровневой работы: взаимодействия с железом, организации памяти, файловой системы и так далее.

В мире Linux каждая запущенная программа — это отдельный процесс. Каждый процесс обладает уникальным номером и собственной «территорией» — виртуальным адресным пространством, в рамках которого содержатся все данные процесса. Поток же — это набор инструкций внутри запущенной программы (процесса), который может быть выполнен отдельно. У потока нет своего уникального идентификатора и адресного пространства — все это он наследует от родительского процесса и делит с другими потоками.


Массовое распространение в Google Play приложений, имеющих проблемы с утечкой памяти, резонно вызовет у пользователей ощущение, что «Android тормозит»
Рис. 1. Список активных процессов в Android
Рис. 1. Список активных процессов в Android

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

Когда в компьютерах (а вслед за ними — в планшетах и телефонах) появились процессоры с несколькими ядрами, программисты внедрили в ОС планировщик задач. Такой планировщик самостоятельно распределяет нагрузку по всем ядрам процессора, исполняя блоки кода параллельно или асинхронно, и тем самым повышает производительность. Поначалу маркетологи даже продавали компьютеры с лозунгом «Два ядра — в два раза быстрее», но, к сожалению, действительности он не соответствует.

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

Вообще, суть многопоточного программирования в том, чтобы максимально задействовать все ресурсы устройства, при этом синхронизируя результаты вычислений. Это не так легко, как может показаться на первый взгляд, но создатели Android добавили в API несколько полезных классов, которые сильно упростили жизнь Java-разработчику.

 

Потоки в Android

Запущенное в Android приложение имеет собственный процесс и как минимум один поток — так называемый главный поток (main thread). Если в приложении есть какие-либо визуальные элементы, то в этом потоке запускается объект класса Activity, отвечающий за отрисовку на дисплее интерфейса (user interface, UI).

В главном Activity должно быть как можно меньше вычислений, единственная его задача — отображать UI. Если главный поток будет занят подсчетом числа пи, то он потеряет связь с пользователем — пока число не досчиталось, Activity не сможет обрабатывать запросы пользователя и со стороны будет казаться, что приложение зависло. Если ожидание продлится чуть больше пары секунд, ОС Android это заметит и пользователь увидит сообщение ANR (application not responding — «приложение не отвечает») с предложением принудительно завершить приложение.

Рис. 2. ANR-сообщение
Рис. 2. ANR-сообщение

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

 

Thread и Runnable

Базовым классом для потоков в Android является класс Thread, в котором уже все готово для создания потока. Но для того, чтобы что-то выполнить внутри нового потока, нужно завернуть данные в объект класса Runnable. Thread, получив объект этого класса, сразу же выполнит метод run.

public class MyRunnable implements Runnable {
  String goal;
  public MyRunnable(String goal) {
    this.goal=goal;
  }
  @Override
  public void run() {
    getData(goal);
  }
}
...
MyRunnable runnable = new MyRunnable("do_smth");
new Thread(runnable).start();

Но при такой организации сложно использовать всю силу дополнительных потоков — нельзя ни поменять задачу, ни поcмотреть результат вычислений. Хотя все это происходит в едином адресном пространстве, у Java-разработчика нет возможности просто так получить ресурсы соседних потоков.

 

Looper

Было бы классно уметь перекидывать данные из одного потока в другой. В Android, как и любой Linux-системе, это возможно. Один из доступных в Android способов — это создать очередь сообщений (MessageQueue) внутри потока. В такую очередь можно добавлять задания из других потоков, заданиями могут быть переменные, объекты или кусок кода для исполнения (Runnable).

Message msg = new Message();
Bundle mBundle = new Bundle();
mBundle.putString("KEY", "textMessage");
msg.setData(mBundle);

Чтобы организовать очередь, нужно воспользоваться классами Handler и Looper: первый отвечает за организацию очереди, а второй в бесконечном цикле проверяет, нет ли в ней новых задач для потока.

public class MyLooper extends Thread {
  Integer number;
  public Handler mHandler;
  @Override
  public void run() {
    Looper.prepare();
    mHandler = new Handler() {
      @Override
      public void handleMessage(Message msg) {
        // process incoming messages here
        Log.e("Thread", "#"+number + ": "+msg.getData().getString("KEY"));
      }
    };
    Looper.loop();
  }
}

Запуск такого потока устроен по похожей схеме — создание нового объекта и вызов метода start.

MyLooper myLooper = new MyLooper();
myLooper.start();

После выполнения этого метода создастся новый поток, который заживет своей жизнью. А это значит, что инициализация переменных и создание объектов будут уже идти параллельно с теми вызовами, которые забиты в следующих строчках после команды myLooper.start(). Поэтому перед обращением к очереди в новом потоке нужно немного подождать — объект handler может еще не существовать.

If (myLooper.hanlder!=null) {
  myLooper.mHandler.sendMessage(msg);
}
 

AsynkTask

Загружая или вычисляя что-то в фоне, хорошо бы не только получить результаты, но еще и иметь возможность выводить пользователю информацию о прогрессе. Конечно, все это можно сделать самому с помощью очереди сообщений, но разработчики Android и тут упростили нам жизнь, создав класс AsyncTask.

Продолжение статьи доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи одну статью

Заинтересовала статья, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для статей, опубликованных более двух месяцев назад.


3 комментария

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

WWW: Airmash — масштабное воздушное сражение в твоем браузере

Браузерные игры, которые стоит только открыть, чтобы оказаться в пылу битвы, — это особо б…