Платформа Android Ведущий семинара: Максим Лейкин, компания «МЕРА НН» Работа с потоками Все операции, которые потенциально могут занять существенное время (>5 сек) необходимо выносить в отдельные потоки, чтобы они не тормозили UI Thread Есть несколько способов способов разделить выполнение на несколько потоков: -cоздание потоков Thread/Runnable -использование класса AsyncTask Создание потоков Передача сообщений между потоками Нельзя просто взять и получить доступ к элементам UI из другого потока. В силу модели многопоточности Android, изменять состояние элементов интерфейса разрешается только из того потока, в котором эти элементы были созданы, иначе будет вызвано исключение CalledFromWrongThreadException. На этот случай Android API предоставляет сразу несколько решений. 1. View#post 2. Activity#runOnUiThread 3. Handler View#post protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); WorkingClass workingClass = new WorkingClass(); Thread thread = new Thread(workingClass); thread.start(); } class WorkingClass implements Runnable{ public void run() { //Фоновая работа //Отправить в UI поток новый Runnable textView.post(new Runnable() { @Override public void run() { textView.setText("The job is done!"); } }); } } Activity#runOnUIThread protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); WorkingClass workingClass = new WorkingClass(); Thread thread = new Thread(workingClass); thread.start(); } class WorkingClass implements Runnable{ public void run() { //Фоновая работа //Отправить в UI поток новый Runnable MainActivity.this.runOnUiThread(new Runnable() { @Override public void run() { textView.setText("The job is done!"); } }); } } Handler 1. Создать внутри класса экземпляр класса Handler 2. Переопределить метод handleMessage() 3. Создать новый поток (non-UI thread) 4. В non-UI thread необходимо получить объект сообщение через метод obtainMessage() 5. Послать сообщение в Handler через один из методов sendMessage() Handler Методы sendMessage(): 1) boolean sendMessage(Message msg) – посылает сообщение в конец очереди 2) boolean sendMessageAtFrontOfQueue(Message msg) – посылает сообщение в начало очереди 3) boolean sendMessageAtTime(Message msg, long uptimeMillis) – посылает сообщение в конец очереди в заданное время 4) boolean sendMessageDelayed(Message msg, long delayMillis) - посылает сообщение в конец очереди с задержкой delayMillis миллисекунд по отношению к текущему времени AsyncTask Класс AsyncTask позволяет выполнять «тяжелые» задачи вне UI-потока и передавать в UI-поток результаты работы. Это проще чем через handler , но имеет ряд ограничений. Чтобы работать с AsyncTask, надо создать класс-наследник и в нем переопределить методы: - - doInBackground() – будет выполнен в новом потоке, здесь решаем все «тяжелые» задачи. Т.к. поток не основной - не имеет доступа к UI. onPreExecute() – выполняется перед doInBackground(), имеет доступ к UI onPostExecute() – выполняется после doInBackground() (не срабатывает в случае, если AsyncTask был отменен), имеет доступ к UI onProgressUpdate() – отображает прогресс выполнения в UI-потоке Поток запускается на выполнение из UI-потока методом execute(). AsyncTask При описании класса-наследника AsyncTask надо указать три типа данных: 1) Тип входных данных. Это данные, которые пойдут на вход метода doInBackground() 2) Тип промежуточных данных. Данные, которые используются для вывода промежуточных результатов в методе onProgressUpdate() 3) Тип возвращаемых данных. То, что вернет AsyncTask после работы. Приходи в качестве параметра в метод onPostExecute(), можно получить методом get() Пример: class MyTask extends AsyncTask<String, Integer, Void> { … } AsyncTask Для отмены AsyncTask нужно вызвать метод: public final boolean cancel (boolean mayInterruptIfRunning) Будет произведена попытка отмены задачи. Если задача уже закончила выполняться, уже была отменена раньше или не может быть отменена – метод вернет false. Если задача еще на начала выполняться, то ее выполнение не начнется. Если она уже начала выполняться, то она будет прервана или не прервана в зависимости от значения параметра mayInterruptIfRunning В результате вызова cancel() после того как закончит выполнение метод doInBackground() будет вызван метод onCancelled() вместо onPostExecute() AsyncTask Несмотря на вызов cancel() метод doInBackground() будет выполняться до конца. Если мы этого не хотим, надо внутри этого метода периодически проверять возвращаемое значение метода isCancelled() и самим выходить из метода doInBackground() protected void doInBackground(Void... params) { try { for (int i = 0; i < 5; i++) { Thread.sleep(1); if (isCancelled()) return null; } } catch (InterruptedException e) { Log.d(LOG_TAG, "Interrupted"); e.printStackTrace(); } return null; } Недостатки AsyncTask 1. Жизненный цикл Если Activity создавшая AsyncTask будет уничтожена, AsyncTask не будет уничтожен автоматически. Он продолжает работать, пока его метод doInBackground() не завершится. Если теперь AsyncTask попытается взаимодействовать с компонентами уничтоженной Activity (например, в методе onPostExecute()) программа упадет, т.к. Activity больше не существует. Таким образом, мы всегда должны быть уверены, что мы отменили нашу задачу до того как Activity будет уничтожена. 2. Утечки памяти Так как AsyncTask имеет методы, которые работают в фоновом потоке (doInBackground()), а также методы, которые работают в потоке пользовательского интерфейса (например OnPostExecute()), он сохраняет ссылку на Activity, до тех пор пока работает. Но, если Activity уничтожена, он будет попрежнему хранить эту ссылку в памяти. Это препятствует освобождению памяти garbage collector. Недостатки AsyncTask 3. Потеря результатов Мы потеряем результаты выполнения AsyncTask если наша Activity будет пересоздана. Например, это происходит при повороте экрана. Activity будет уничтожена и создана заново, но наш AsyncTask теперь будет иметь недействительную ссылку на эту Activity, поэтому onPostExecute() не будет иметь никакого эффекта. Чтобы решить эту проблему нужно сохранять ссылку на AsyncTask во время изменения конфигурации Последовательность выполнения AsyncTask Пример: new AsyncTask1().execute(); new AsyncTask2().execute(); В каком порядке выполняются? До API версии 1.6 (Donut): последовательно С API версии 1.6 до API версии 2.3 (Gingerbread): параллельно С API версии 3.0 (Honeycomb) по настоящее время: последовательно А вот так параллельно: public static void execute(AsyncTask as) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR1) { as.execute(); } else { as.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } Intents: Broadcast Intents С помощью Broadcast Intents можно передавать широковещательные сообщения системе. Таким образом, например, операционная система уведомляет приложения о поступившем звонке, уровне заряда батареи, появлении нового wi-fi спота и т.п. Можно создавать свои широковещательные сообщения. public static final String NEW_LIFEFORM_DETECTED = “com.niit.android.intent.action.NEW_LIFEFORM”; Intent intent = new Intent(NEW_LIFEFORM_DETECTED); intent.putExtra(“lifeformName”, lifeformType); intent.putExtra(“longitude”, currentLongitude); intent.putExtra(“latitude”, currentLatitude); sendBroadcast(intent); Intents: Broadcast Intents Прослушивание Broadcast Intents: 1. Создать класс наследуемый от BroadcastReceiver: import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class MyBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //TODO: React to the Intent received. } } Внимание! 1) Метод onReceive() должен выполняться не более 5 сек. иначе ОС снимет приложение, как не отвечающее на запросы. 2) Для реакции на Broadcast Intent приложение-ресивер не обязано быть запущено, при поступлении Broadcast Intent оно запустится автоматически Intents: Broadcast Intents 2. Зарегистрировать его в AndroidManifest.xml: <receiver android:name=”. MyBroadcastReceiver ”> <intent-filter> <action android:name=” com.niit.android.intent.action.NEW_LIFEFORM”/> </intent-filter> </receiver> или в коде: public static final String NEW_LIFEFORM_DETECTED = “com.niit.android.intent.action.NEW_LIFEFORM”; IntentFilter filter = new IntentFilter(NEW_LIFEFORM_DETECTED); MyBroadcastReceiver r = new MyBroadcastReceiver (); registerReceiver(r, filter); 3. Когда ресивер больше не нужен его нужно разрегистрировать: unregisterReceiver(r); Intents: Native Broadcast Actions • ACTION_BOOT_COMPLETED • ACTION_CAMERA_BUTTON • ACTION_DATE_CHANGED • ACTION_TIME_CHANGED • ACTION_GTALK_SERVICE_CONNECTED • ACTION_GTALK_SERVICE_DISCONNECTED • ACTION_MEDIA_BUTTON • ACTION_MEDIA_EJECT • ACTION_MEDIA_MOUNTED • ACTION_MEDIA_UNMOUNTED • ACTION_SCREEN_OFF • ACTION_SCREEN_ON • ACTION_TIMEZONE_CHANGED Полный список: http://developer.android.com/reference/android/content/Intent.html Services Cервис (служба) – это программа, которая работает в фоне и не использует UI. Запускать и останавливать сервис можно из приложений и других сервисов. Также можно подключиться к уже работающему сервису и взаимодействовать с ним. Сервис может быть запущен двумя способами: - клиент (например, активность) вызывает метод Context.startService() . В этом случае сервис начинает выполняться и выполняется до тех пор пока не будет вызван метод Context.stopService() или stopSelf() - клиент вызывает метод Context.bindService(), чтобы получить доступ к сервису и получает объект IBinder, который ему возвращает сервис из метода onBind(Intent), через который и осуществляется взаимодействие с сервисом. Сервис будет работать ровно столько времени, сколько клиент будет удерживать соединение через IBinder и сервис будет остановлен, когда клиент уничтожит объект. Возможно одновременное подключение нескольких клиентов к сервису, в этом случае сервис работает пока остаются подключенные клиенты. Services, processes, threads Важно понимать: 1. Сервис - это не отдельный процесс. Сервис запускается в рамках приложения частью которого он является 2) Сервис - это не поток. По умолчанию он запускается в главном потоке приложения (hosting app). Это потенциально может приводить к возникновению ANR (Application Not Responding). Если в сервисе выполняются «тяжелые» операции, то лучше выполнять их в отдельном потоке внутри сервиса. Если приложению необходимо выполнять операции в фоновом режиме, но только во время работы приложения – лучше создавать поток (или AsyncTask), а не сервис! Services Services Started (приложение стартует и завершает сервис) Bound (приложение подключается (bind) и отключается (unbind) от сервиса) Сервис может быть одновременно и started и bound Services: lifecycle Чтобы создать сервис надо унаследоваться от класса Service и переопределить коллбэки (не обязательно все): • onStartCommand() – вызывается, когда клиент вызывает startService(). Остановка сервиса в этом случае выполняется вручную методом stopSelf() или stopService() • onBind() – вызывается, когда клиент подсоединяется к сервису методом bindService(). В имплементации необходимо предоставить интерфейс, который клиенты используют для связи с сервисом (IBinder). Этот метод надо реализовывать всегда, если сервис не допускает коннекта надо просто вернуть null. • onCreate() – вызывается только при первом обращении к сервису (на создание или на коннект) перед методами onStartCommand() или onBind(), если сервис уже запущен не вызывается. • onDestroy() – вызывается когда сервис больше не нужен (нет ни одного коннекта или был вызван stopService()), выполняет все действия по освобождению ресурсов (остановка потоков, разрегистрация слушателей и т.д.) Services: lifecycle Services: lifecycle public class ExampleService extends Service { int mStartMode; // indicates how to behave if the service is killed IBinder mBinder; // interface for clients that bind boolean mAllowRebind; // indicates whether onRebind should be used public void onCreate() { // The service is being created } public int onStartCommand(Intent intent, int flags, int startId) { // The service is starting, due to a call to startService() return mStartMode; } public IBinder onBind(Intent intent) { // A client is binding to the service with bindService() return mBinder; } public boolean onUnbind(Intent intent) { // All clients have unbound with unbindService() return mAllowRebind; } public void onRebind(Intent intent) { // A client is binding to the service with bindService(), // after onUnbind() has already been called } public void onDestroy() { // The service is no longer used and is being destroyed } } Services: manifest <manifest ... > ... <application ... > <service android:name="string" android:enabled=["true" | "false"] android:exported=["true" | "false"] android:icon="drawable resource" android:isolatedProcess=["true" | "false"] android:label="string resource" android:permission="string" android:process="string" > <intent-filter> … </intent-filter> </service> </application> </manifest> Services: started services Intent intent = new Intent(this, ExampleService.class); startService(intent); public class ExampleService extends Service { … public int onStartCommand(Intent intent, int flags, int startId) { Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show(); // If we get killed, after returning from here, restart return START_STICKY; } } Services: started services Intent intent = new Intent(this, ExampleService.class); stopService(intent); или public class ExampleService extends Service { … public void processServiceCode() { … … stopSelf(); } } Services: bound services Bound service – позволяет клиентам подключаться к нему, посылать запросы, получать ответы и т.п. Все это возможно как на уровне одного процесса (Local Service) так и между процессами через механизм IPC (Remote Service) Bound service выполняется пока есть хотя бы один подключенный к нему клиент. Для создания bound service необходимо переопределить метод onBind() в классе, унаследованном от Service. Этот метод должен вернуть объект IBinder, определяющий интерфейс для взаимодействия клиентов с сервисом. Клиент подключается к сервису с помощью метода bindService(). Он должен имплементировать интерфейс ServiceConnection, который следит за соединением. Метод bindService() асинхронный (возвращается немедленно), но когда соединение установлено вызывается коллбэк onServiceConnected() реализации ServiceConnection, в который приходит объект IBinder. Services: bound services Если к сервису подключаются несколько клиентов, onBind() вызывается только один раз для первого клиента. Для остальных клиентов возвращается тот же самый объект IBinder. Когда последний клиент отключается от сервиса, система выгружает сервис из памяти (если он не был одновременно запущен методом startService()). Services: local bound services Порядок создания: 1) На стороне сервиса: • создать экземпляр класса, унаследованного от Binder, который либо сам содержит public методы, для вызова из клиента, либо возвращает ссылку на объект Service, который содержит public методы • вернуть из метода onBind() экземпляр Binder 2) На стороне клиента: • в методе onServiceConnected() получить экземпляр Binder, через этот объект получить ссылку на сервис и вызывать методы сервиса. Services: local bound services class Client extends Activity { LocalService mService; boolean mBound = false; MyServiceConnection mConnection = MyServiceConnection(); protected void onStart() { super.onStart(); Intent intent = new Intent(this, LocalService.class); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } class MyServiceConnection implements ServiceConnection { public void onServiceConnected(ComponentName className, IBinder service) { // We've bound to LocalService, cast the IBinder and get LocalService instance LocalBinder binder = (LocalBinder) service; class LocalService extends Service mService = binder.getService(); { private final IBinder mBinder = new LocalBinder(); mBound = true; public IBinder onBind(Intent intent) { } return mBinder; } } } public class LocalBinder extends Binder { LocalService getService() { return LocalService.this; } } } Services: remote bound services Порядок создания: 1) На стороне сервиса: • сервис реализует интерфейс Handler, в котором обрабатываются сообщения от клиентов • Handler используется для создания объекта Messenger • в методе onBind() с помощью Messenger создается IBinder и возвращается клиенту • получать сообщения от клиентов и обрабатывать их в методе handleMessage() 2) На стороне клиента: • клиенты используют полученный IBinder для создания своего объекта Messenger, который будет использоваться для отправки сообщений сервису Services: remote bound services public class MessengerService extends Service { /** Command to the service to display a message */ static final int MSG_SAY_HELLO = 1; class IncomingHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SAY_HELLO: Toast t = Toast.makeText(getApplicationContext(), "hello!", Toast.LENGTH_SHORT); t.show(); break; default: super.handleMessage(msg); } } } final Messenger mMessenger = new Messenger(new IncomingHandler()); public IBinder onBind(Intent intent) { Toast.makeText(getApplicationContext(), "binding", Toast.LENGTH_SHORT).show(); return mMessenger.getBinder(); } } Services: remote bound services public class ActivityMessenger extends Activity { Messenger mService = null; boolean mBound; private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { mService = new Messenger(service); mBound = true; } public void onServiceDisconnected(ComponentName className) { mService = null; mBound = false; } }; public void sayHello(View v) { if (!mBound) return; // Create and send a message to the service, using a supported 'what' value Message msg = Message.obtain(null, MessengerService.MSG_SAY_HELLO, 0, 0); try { mService.send(msg); } catch (RemoteException e) { e.printStackTrace(); } } Services: remote bound services public class ActivityMessenger extends Activity { … @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } @Override protected void onStart() { super.onStart(); // Bind to the service bindService(new Intent(this, MessengerService.class), mConnection, Context.BIND_AUTO_CREATE); } @Override protected void onStop() { super.onStop(); // Unbind from the service if (mBound) { unbindService(mConnection); mBound = false; } } } Services: remote bound services public class ActivityMessenger extends Activity { … @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } @Override protected void onStart() { super.onStart(); // Bind to the service bindService(new Intent(this, MessengerService.class), mConnection, Context.BIND_AUTO_CREATE); } @Override protected void onStop() { super.onStop(); // Unbind from the service if (mBound) { unbindService(mConnection); mBound = false; } } } Services: remote bound services Важно: 1. Всегда необходимо обрабатывать RemoteException, когда возникает когда связь с сервисом обрывается. 2. Нужно отключаться от сервиса, когда он больше не нужен • Если сервис нужен пока активность видна на экране, нужно подключаться в onStart() и отключаться в onStop() • Если сервис нужен даже когда активность в бэкграунде, нужно подключаться в onCreate() и отключаться в onDestroy() • Не рекомендуется подключаться в onResume() и отключаться в onPause(), т.к. эти методы выполняются слишком часто Services: started & bound services Сервис может быть одновременно и started и bound. Т.е. его можно запустить методом startService() или подключиться к нему методом bindService() В этом случае в коде сервиса необходимо переопределять и onBind() и onStartCommand(). Система не будет разрушать сервис при отключении всех клиентов, если он был запущен через startService(), вместо этого он должен останавливаться сам через методы stopSelf() или stopService(). Также, если сервис и started и bound, когда вызывается onUnbind() можно вернуть true если вы хотите, чтобы в следующий раз при подключении клиента был вызван onRebind() вместо onBind(). Services: started & bound services Intent Services Android API предоставляет класс IntentService, расширяющий стандартный Service, но выполняющий обработку переданных ему данных в отдельном потоке. При поступлении нового запроса (в виде Intent, пришедшего в onStartCommand()) IntentService сам создаст новый поток и вызовет в нем метод IntentService#onHandleIntent(Intent intent), который можно переопределить. Если при поступлении нового запроса обработка предыдущего еще не закончилась, он будет поставлен в очередь. Когда последний Intent из очереди обработан, сервис сам завершает свою работу.