czwartek, 23 października 2014

Pojedynek z Androidem - odc. 15 asynchroniczne programowanie i serwisy (mała retrospekcja)

Po wchłonięciu gorących nowości z ostatniego buildu Windows 10 przyszła pora na wydanie kolejnego odcinka poświęconemu systemowi Android. Dziś zrobię trochę rozważań opartych… na tym, co już było. Taka mała retrospekcja.

Jeśli chodzi o responsywność to filozofia przypomina w dużym stopniu tę znaną z Windows, przy czym Microsoft ma tylko asynchroniczne WinRT API i programista - choćby nawet chciał - to nie przyblokuje interfejsu np. operacją na pliku. W Androidzie trzeba być świadomym AsyncTask.  Co do sposobu jego wykonywania, to mamy historyczne zróżnicowanie (sekwencyjnie, równolegle, potem znów sekwencyjnie z możliwością jawnego wybrania). W Windows TPL / WinJS pozwalają wygodnie sterować ciągami operacji.

Serwis widziany z poziomu różnych aplikacji, procesów może być udogodnieniem dla twórców aplikacji, ale - IMHO - też i jakąś furtką dla osób o niezbyt dobrych zamiarach.

Wątki w serwisie… cóż jak dla mnie trochę przesyt różnych tworów takich jak zagnieżdżone klasy, implementacje interfejsu Runnable.  A wszystko to okraszone różnymi zmiennymi.  Zarządzanie cyklem życia serwisu obejmuje potrzebę pamiętania wyrafinowanych opcji przy wznawianiu jego pracy.

Wyświetlanie wiadomości toast z poziomu serwisu wymaga stworzenia wątku z pomocą klasy Handler i loopera. Uff… W Windows nie widzę, żeby były jakieś różnice między korzystaniem z notyfikacji toast w aplikacji i serwisie. W Android przy wysyłania notyfikacji z serwisu (serwisy foreground) też musimy postąpić nieco inaczej niż w UI.  W Windows znów nie ma różnic dla notyfikacji tile.  Android nie jest tutaj mistrzem prostoty.

Wyzwalanie serwisów na zdarzenia systemowe…  to jedna z głównych koncepcji tasków w tle na platformie Windows.  W Android implementujemy odpowiednią klasę do przechwycenia systemowego zdarzenia, a potem w jej metodzie onReceive jawnie wywołujemy serwis przez startService.  Microsoft zaenkapsulował nam taki przypadek w postaci predefiniowanych triggerów, w Androidzie możemy sobie sami to oprogramować.

 

 

Wyzwania dla responsywnego “user experience”

Responsywność

  • Musimy utrzymywać dostępność głównego wątku by przechwytywać zdarzenia
  • Przewodniki dla Android zalecają by handlery zajmowały mniej niż 200ms (preferowane jest mniej niż 100ms)
  • Komunikaty ANR pojawiają się, jeśli wątek główny zajęty będzie dłużej niż 5 sekund

Trzy strategie na podtrzymywanie responsywności

  • upewnienie się, że operacje potencjalnie blokujące nie występują w głównym wątku
    • klasa Strictmode
      • od Android 2.3 / API Level 9
      • rodzaje przechwytywanych operacji - setThreadPolicy(…)
      • sposób przechwytywania - setVmPolicy(…)
      • najczęściej używane - enableDefaults
      • tylko na czas developmentu
  • używanie dodatkowych wątków
    • użyteczne dla nie za długich, długo trwających operacji
      • od 100 ms do kilku sekund
      • wątki trwające więcej niż kilka sekund mogą być zabite przez system operacyjny
    • międzywątkowy dostęp do UI
      • Activity.runOnUiThread
      • View.post
    • stosowanie dedykowanych wątków zwykle nie jest najlepszym pomysłem na poprawę UI
      • tworzenie/niszczenie wątku jest kosztowne
      • dość złożony kod
    • lepiej stosować asynchroniczne taski specjalne dedykowane do tego rodzaju zadań – AsyncTask
      • fazy pracy
        • onPreExecute - odczytanie informacji z UI w głównym wątku
        • doBackground
        • publishProgress / onProgressUpdate
        • onPostExecute
      • lepsza wydajność niż dedykowane wątki (wykorzystanie puli wątków)
      • taski wykonujące się dłużej niż kilka sekund mogą być zabite przez OS
      • duże różnice w wykonywaniu - domyślne zachowanie puli wątków zależy od wersji systemu
        • taski wykonywane są zawsze sekwencyjnie ( Android 1.0 / API 1  -  Android 1.5 / API 3)
        • taski mogą wykonywać się równolegle (Android 1.6 / API 4  -  Android 2.2 / API 8)
        • taski wykonywane są zawsze sekwencyjnie (Android 2.3  / API 9  i nowszy)
      • od Android 3.0 / API 11 można określić sposób wykonywania taska
        • AsyncTask.executeOnExecutor
        • AsyncTask.SERIAL_EXECUTOR - sekwencyjne wykonywanie tasków
        • AsyncTask.THREAD_POOL_EXECUTOR - równoległe wykonywanie tasków
      • więcej:  odc 4
  • używanie serwisów

 

public class XActivity extends Activity  {          

       …

       private void updateDisplay(String message)  {

               _defaultTextViewTemp = message;

              _defaultTextView.post(new Runnable()  {

                      public void run()  {

                              _defaultTextView.setText(_defaultTextViewTemp);

                      }

              }

       }

}

 

_asyncTaskWorker = new AsyncTaskWorker();

_asyncTaskWorker.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, _param);

 

Implementacja długotrwających operacji jako serwisów

Operacja “długotrwająca” - na tyle długa, że użytkownik może przełączyć się na inną aplikację

W serwisie mogą być określone preferencje co do wykonywania

  • może być długo trwającą operacją w tle
  • może mieć taką samą ważność jak UI aktualnie używanej aplikacji

Kategorie serwisów

  • uruchomione (started services)
    • duża kontrola nad swoim czasem życia
    • ograniczona interakcja z innymi komponentami
    • większość serwisów
  • związane (bound services)
    • czas życia dopasowany do komponentów, które mają do niego dostęp
    • duża interakcja z komponentami, które mają do niego dostęp

IntentService

  • najprostsza implementacja (wystarczy konstruktor i nadpisanie onHandleIntent)
  • uruchamiany na żądanie (na czas przetwarzania przychodzących żądań)
  • obsługa scenariusza kolejkowania długo trwającej operacji w tle
  • tworzy pojedynczy wątek roboczy po wywołaniu startService
    • przy dodatkowych wywołaniach startService podczas wykonywania żądania są serializowane
  • wątek roboczy jest kończony po przetworzeniu wszystkich żądań
  • tworzony jest nowy wątek roboczy, jeśli pojawią się nowe żądania po zamknięciu
  • więcej: odc 5

<manifest … package=”xxx.yyy”>

         <application>

                    <service android:name=”.serviceClass” android:label=”nazwa używana w widokach dla użytkownika” />

         </application>

</manifest>

Używanie serwisów między procesami

Serwis

  • wykonuje się w procesie, w którym jest zdefiniowany
  • może być wywoływany z innych komponentów będących w jego procesie
  • może być wywoływany przez komponenty z innych procesów (z innej aplikacji)
  • może obsługiwać równocześnie żądania z różnych procesów
  • uzupełnienie do odc 5
  • aby być widoczny dla innych procesów musi definiować filtr intencji
    • zwykle test akcji, ale można także umieścić kategorię i/lub dane
    • jeśli zachodzi potrzeba proces serwisu jest uruchamiany
    • jeśli proces serwis już jest uruchomiony wykorzystywana jest jego istniejąca instancja

<service …>

         <intent-filter>

                  …

        </intent-filter>

</service>

 

Zarządzanie cyklem życia serwisu

Serwisy uruchomione (started services) mogą zarządzać swoim cyklem życia

Cykl życia w trzech metodach klasy Service

  • onCreate (każda instancja klasy dokładnie jedno wywołanie, po Context.startService jeśli nie było instancji serwisu)
  • onStartCommand  (przy wywoływaniu Context.startService)
  • onDestroy (po wywołaniu Context.stopService, stopSelf (przez sam serwis) lub przez system operacyjny ze względu na zasoby)
  • więcej:  odc 5

Started services nie używają metody onBind (w nadpisaniu zwracamy null)

Wątki w serwisach

  • wszystkie metody callback serwisu wykonują się w głównym wątku aplikacji (tym samym co UI)
  • jesteśmy odpowiedzialni za przekazanie pracy do innego wątku
    • można użyć jawnie tworzonego wątku
    • można użyć jedną z klas, która implementuje ExecutorService
      • implementacjami ExecutorService są pule wątków
      • używamy klasy Executors to tworzenia instancji ExecutorService
      • przykład: odc 13

Executors

  • newFixedThreadPool(…)
  • newScheduledThreadPool(…)
  • newSingleThreadExecutor()

public class WorkerService extends Service  {

        …

        ExecutorService _executorService;

       

        @Override

        public void onCreate()  {

                 …

                 _executorService = Executors.newSingleThreadExecutor();

        }   

 

        @Override

        public int onStartCommand(Intent intent, int flags, int startId)  {

                  _executorService.execute(new Runnable()  {

                          public void run()  {

                                  …

                          }

                 });

                 return Service.START_NOT_STICKY;

        }  

 

         …

}

Serwis ma dwa mechanizmy na zarządzanie swoim cyklem życia:

  • żądanie zatrzymania siebie
    • jawne
      • serwis wywołuje stopSelf
      • system automatycznie wywołuje onDestroy i zatrzymuje serwis
    • warunkowe
      • serwis może wskazać, że chciałby się zatrzymać jeśli nie ma oczekującej pracy
      • serwis wywołuje stopSelfResult przyjmujące numer żądania z onStartCommand
        • serwis się zatrzyma tylko wtedy, jeśli żadne żądania nie pojawią się po danym żądaniu
  • onStartCommand - zwracana wartość określa, co system powinien zrobić, jeśli proces serwisu zostanie zabity (z powodu zasobów)
    • START_NOT_STICKY - jeśli serwis został zabity, nie jest restartowany (przy większych zasobach) aż do wywołania startService
    • START_STICKY - jeśli serwis został zabity, automatycznie jest restartowany, gdy zasoby są dostępne, intencja przekazywana w onStartCommand ma wartość null
    • START_REDELIVER_INTENT - jeśli serwis został zabity przed wywołaniem stopSelfResult dla ostatniego żądania, automatycznie jest restartowany, jeśli zasoby są dostępne, przekazywana jest intencja ostatnio przekazana do serwisu zanim został zabity
    • START_STICKY_COMPATIBILITY - kompatybilność z zachowaniem serwisu z Android 1.6 / API 4 lub wcześniejszym, automatyczny restart serwisu po zabiciu, jeśli zasoby są dostępne, podczas restartu metoda onStartCommand może zostać wywołana, konieczność nadpisania przestarzałej metody onStart
  • patrz: odc 5 

public class WorkerService extends Service  {

        …   

        ExecutorService _executorService;

        ScheduledExecutorService _scheduledStopService;

       

        @Override

        public void onCreate()  {

                 …

                 _executorService = Executors.newSingleThreadExecutor();

                 _scheduledStopService = Executors.newSingleThreadScheduledExecutor();

        }   

 

        @Override

        public int onStartCommand(Intent intent, int flags, int startId)  {

                 serviceRunnable runnable = new serviceRunnable(this, startId);

                  _executorService.execute(runnable);

                 return Service.START_NOT_STICKY;

        }

 

        …

 

        class serviceRunnable implements Runnable  {

                 WorkerService _theService;

                 int _startId;

                 public serviceRunnable(WorkerService theService, int startId)  {

                          _theService = theService;

                          _startId = startId;

                 }

                 public void run()  {

                           …

                          //_theService.stopSelf();

                         // _theService.stopSelfResult(_startId);

                         // opóźnione zatrzymanie

                         delayedStopRequest stopRequest = new delayedStopRequest(_theService, _startId);

                         _theService._scheduledStopService.schedule(stopRequest, 5, TimeUnit.MINUTES);

                 }

        }

 

        class delayedStopRequest implements Runnable  {

                 WorkerService _theService;

                 int _startId;

                 public delayedStopRequest(WorkerService theService, int startId)  {

                          _theService = theService;

                          _startId = startId;

                 }

                 public void run()  {

                           _theService.stopSelfResult(_startId);

                 }

        }      

}

 

Interakcja z serwisami

Ogólnie, serwisy powinny wchodzić w minimalną interakcję z użytkownikami

  • nie należy próbować oddziaływać bezpośrednio na instancję aktywności
    • przechowywanie referencji nie jest bezpieczne
    • instancje aktywności są często odtwarzane na nowo
  • należy używać technik będących poza interakcją z użytkownikiem
    • wiadomości toast
    • notyfikacje

Wyświetlanie wiadomości toast z poziomu serwisu wymaga specjalnej obsługi

  • musi być wywoływana z wątku, który ma looper (HandlerThread)
  • pomocna jest tu klasa Handler
    • pozwala jawnie tworzyć HandlerThread
    • najłatwiejsze w użyciu Context.getMainLooper by dostać się do głównego HandlerThread z UI

public class WorkerService extends Service  {

        …

        class serviceRunnable implements Runnable  {

                WorkerService _service;                

                …

                Handler _handler;

                String _statusMessage;

                …

                public void run()  {

                      setupHandler();                    

                      …

                      updateStatus(“xxx”);

                      …

                }

                void setupHandler()  {

                       _handler = new Handler(_service.getMainLooper());

                }

                void updateStatus(String message)  {

                        _statusMessage = message;

                        _handler.post(new Runnable() {

                                public void run()  {

                                      Toast toast = Toast.makeText(_service, _statusMessage , Toast.LENGTH_LONG);

                                       toast.show();

                                }

                        });

                }

         }

}

Serwisy, które wpływają bezpośrednio na user experience

  • serwisy foreground 
    • serwis, którego użytkownik jest bezpośrednio świadomy
      • np. odtwarzacz muzyki
    • informacje serwisu pojawiają się w status bar (wyświetlanie notyfikacji przez serwis)
      • wyświetlenie ikony
      • wyświetlenie tekstu (dodatkowa informacja po rozwinięciu; napis podczas pierwszego pojawienia)
      • możliwość wyzwolenia akcji, kiedy użytkownik wchodzi w interakcję z paskiem statusu
    • serwis może się wchodzić i wychodzić z przebywania w stanie foreground
      • sam może wywołać na sobie startForeground / stopForeground
      • wywołanie stopForeground powoduje usunięcie notyfikacji z paska statusu (serwis nadal kontynuuje swoje działanie)
      • jeśli zostanie zamknięty z jakiegokolwiek powodu, system automatycznie wywołuje stopForeground
    • notyfikacja jest głównym sposobem na interakcję z użytkownikiem
      • jak wszystkie serwisy, serwis foreground nie ma interfejsu użytkownika
      • możliwość modyfikowania notyfikacji w dowolnym czasie za pomocą NotificationManager
    • więcej: odc 9

public class WorkerService extends Service  {

         …

         final int _notificationId = 1;

         Notification _foregroundNotification;

         NotificationManager _notificationManager;

         …

         @Override

         public void onCreate() {

                …

               _notificationManager = (NotificationManager)  this.getSystemService(Context.NOTIFICATION_SERVICE);

 

                startInForeground();

         }

         …

         void startInForeground()  {

                 int notificationIcon = R.drawable.icon24x24;

                 String notificationTickerText = “xxx”;

                 long notificationTimestamp = System.currentTimeMillis();

                 _foregroundNotification = new Notification(notificationIcon, notificationTickerText, notificationTimestamp);

 

                 String notificationTitleText = “yyy”;

                 String notificationBodyText = “zzz”;

                 Intent intent = new Intent(this, XActivity.class);

                 PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

                 _foregroundNotification.setLatestEventInfo(this, notificationTitleText, notificationBodyText, pendingIntent);

 

                 startForeground(_notificationId, _foregroundNotification);

         }

         void setStatusIcon(int iconId)  {

                 _foregroundNotification.icon = iconId;

                 _notificationManager.notify(_notificationId, _foregroundNotification);

          }

         class serviceRunnable implements Runnable  {

                  WorkerService _service;

                  …

                  public void run()  {

                         _service.setStatusIcon(R.drawable.green24x24);

                         …

                         _service.setStatusIcon(R.drawable.icon24x24);

                  }

         }

}

Serwisy i systemowa aktywność

  • zamykanie przy wyłączaniu urządzenia
  • automatyczny start przy bootowaniu urządzenia
  • zatrzymywanie/uruchamianie korzystania z zasobów sieciowych jeśli urządzenie włącza/wyłącza tryb samolotowy
  • niski stan baterii
  • użytkownik podłącza/odłącza urządzenie do/z zasilania sieciowego

Sygnalizowanie zazwyczaj za pomocą broadcast intents.

Implementujemy broadcast receiver. W metodzie onReceive wywołujemy startService.

Przykład: odc 5

Brak komentarzy: