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
- klasa Strictmode
- 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
- fazy pracy
- użyteczne dla nie za długich, długo trwających operacji
- 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
- jawne
- 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
- serwis, którego użytkownik jest bezpośrednio świadomy
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:
Prześlij komentarz