W tym odcinku zaczynam zgłębiać komponenty wchodzące w skład app-ki. Pojawiają się tematy związane z asynchronicznością, wątkami, pracą w tle, które będą kontynuowane także w następnym poście. Kilka słów, uwag, które rzucam na gorąco:
Serwis - co to jest? Porównując do Windows/Windows Phone jakby odpowiednik background taska, ale są zasadnicze różnice. W Android serwis może wykonywać się ciągle do końca świata, w Windows/Windows Phone własne serwisy wykonują się tylko krótko raz na jakiś czas lub dłużej, ale przy spełnionych określonych warunkach.
Broadcast receiver – mam nadzieję, że dowiem się o tym tworze w przyszłości więcej, na razie kojarzy mi się znowu z background taskiem wyzwalanym przez jakieś systemowe zdarzenia
Domyślnie wszystko w jednej app-ce działa w głównym wątku UI. Tak więc idąć najprościej napiszemy serwis, który wykonując dłuższą operację… zablokuje interfejs użytkownika. Aby to działało naprawdę w tle trzeba trochę opisać się kodu z użyciem HandlerThread.
Sposób na responsywną aplikację - w Windows/Windows Phone mamy async w .NET/C++ albo then/done w WinJS, które opakowują WinRT API, które w większości jest asynchroniczne (dla każdej operacji mogącej chwilkę potrwać). Bardziej jawną wielowątkowość też mamy oczywiście. A Android? Android ma trochę bardziej urozmaicone podejście. Detekcja, że aplikacja nie odpowiada lub klasa StrictMode wykrywająca długotrwające operacje w wątku UI nie wydają się przekonujące, bo kojarzą się z gaszeniem zaistniałego już pożaru wynikłego zbyt liberalnego API. W Windows nie zablokujemy operacją dyskową czy sieciową UI ponieważ dostępne API jest tylko w postaci asynchronicznej. A więc nie zaprószajmy ognia w Androidzie i stosujmy AsyncTask, wątki oraz loopery z handlerami. Te dwa pierwsze wydają się jakimiś bardziej bezpośrednimi odpowiednikami porównując z aplikacjami Windows Store. Tak, Android ma coś takiego jak AsyncTask i nawet podobnie jak async zaczyna i kończy pracę w wątku UI, a także na czas notyfikacji o progresie! Może nieco bardziej toporne, co wynika z ograniczeń języka Java w stosunku do C#, ale zasada ta sama. Hurra! Loopery z handlerami stanowią ponoć jakiś odpowiednik message loop z Windows, aczkolwiek w wysokopoziomowym Windows Store (.NET/JS) nie zchodzimy tak głęboko, a nasłuchiwanie zdarzeń np. o zmianie lokalizacji realizujemy za pomocą prostego API ze zwykłymi zdarzeniami (które ewentualnie możemy wrzucić do taska w tle lub skorzystać z systemowego API do dłuższego śledzenia).
Poniżej tradycyjnie już trochę notatek.
Model komponentów i aplikacji
Serwisy
Aktywności nie mogą zagwarantować wykonywania zadań w tle ani nasłuchiwania zdarzeń w tle. Ich wykonywanie zależy ściśle od interakcji z użytkownikiem.
Serwisy mogą wykonywać się w tle
- normalnie mogą wykonywać się aż skończą swoją pracę
- dziedziczymy po klasie Service
- w manifeście dodajemy element <service> (opcjonalnie z filtrem intencji określającym sposób uruchomienia serwisu)
- wywołujemy przez metodę startService z odpowiednim Intent
public class XService extends Service {
}
<service
android:name=”.XService”>
</service>
Intent intent = new Intent(this, XService.class);
startService(intent);
Broadcast receivers
Są automatycznie wykonywane przez system
- wykonują się w tle, aby przechwycić Intent (monitorowanie systemowych zdarzeń np. zmian w sieci)
- dziedziczymy po klasie BroadcastReceiver (nadpisujemy metodę onReceive)
- rejestrujemy umieszczając <receiver> w manifeście
- zawiera filtr intencji wskazujący na zdarzenie
- można alternatywnie zarejestrować w runtime metodą registerReceiver
public class XReceiver extends BroadcastReceiver {
public void onReceive(…) {
}
}
<receiver android:name=”.XReceiver”>
<intent-filter>
<action … />
</intent-filter>
</receiver>
Proces i komponenty
Wszystkie komponenty w pojedynczej aplikacji współdzielą ten sam proces (aktywności, serwisy, broadcast receivery)
- Typy komponentów nie rozwiązują w niejawny sposób problemów z wątkami
- Wszystkie typy komponentów działają domyślnie w głównym wątku aplikacji
Zarządzanie zasobami
- czas życia aktywności
- niszczone po wywołaniu metody finish
- typowane do destrukcji, gdy nie są na pierwszym planie
- czas życia serwisu - w większości przypadków aż do jawnego zatrzymania
- czas życia broadcast receiver-a
- stworzony do uruchomienia na krótki okres czasu
- typowany do destrukcji jeśli wykonuje się więcej niż 10 sekund
- Czas życia procesu zależy od życia zawieranych przez niego komponentów
- procesy Androida są zawsze zamykane przez system
- proces bez aktywnych komponentów jest niszczony
- Tradycyjne zarządzanie zasobami w oparciu o proces nie jest wystarczające
- w procesie mogą pozostawać zasoby z pozamykanych już komponentów
- Android zabezpiecza się przed takimi “wyciekami” wiążąc poprzez kontekst zasoby z komponentami
- Kontekst jest blisko związany z typami komponentów
- klasy Activity i Service dziedziczą z klasy Context
- klasa BroadcastReceiver otrzymuje referencję na Context
- Zapewnia dostęp do większości informacji o systemie, aplikacji, zasobach
- dostęp do plików
- lokalizacje w app storage
- systemowe serwisy
- informacje o aplikacji
- Zapewnia, że zasoby są automatycznie sprzątane razem z komponentem
- Kontekst jest blisko związany z typami komponentów
public class XReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
boolean state = intent.getBooleanExtra(“state”, false);
…
}
}
XReceiver receiver = new XReceiver();
receiver.setLabel(“XXX”);
IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(receiver, filter); //w kontekście aktywności
Powiązanie Context-u z czasem życia procesu
Każda instancja Context-u może dostarczyć referencję do Context-u dostępnego dla całej aplikacji
- Context.getApplicationContext()
- Powiązany zasób istnieje dopóki nie zostanie jawnie usunięty lub do czasu, gdy istnieje proces
XReceiver receiver = new XReceiver();
receiver.setLabel(“XXX”);
IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
Context appContext = getApplicationContext();
appContext.registerReceiver(receiver, filter); //w kontekście aplikacji
Obiekt aplikacji
Każda aplikacja ma pojedynczą instancję klasy Application
- tworzona przed każdą aktywnością, serwisem, broadcast receiverem
- jedna instancja istnieje przez życie procesu
- możliwość dostarczenia własnej klasy
- musi dziedziczyć po klasie Application
- trzeba wyspecyfikować klasę w manifeście: <application android:name=”.XApp”
- odbiera notyfikacje callbacków
- może przechowywać stan aplikacji
Stan aplikacji alternatywnie można przechowywać w singletonie.
Tworzenie responsywnych aplikacji
Główny wątek jest krytyczny dla user experience
- zarządza interfejsem użytkownika
- klucz do prawie każdego innego zachowania aplikacji (domyślnie także w serwisach, broadcast receiverach, callbackach cyklu życia, powrocie do strony startowej, wysyłaniu intencji)
Dwa główne mechanizmy Androida zabezpieczające główny wątek
- Detekcja
- Dialog ANR (App Not Responding)
- klasa StrictMode
- Odciążenie
- klasa AsyncTask
- wątki
- loopers & handlers
StrictMode wykrywa rzeczy, które mogą wpłynąć na responsywność
- potencjalnie blokujące operacje w sieci i local storage (dysk/flash)
- ustawia politykę blokowania operacji (rodzaj operacji, sposób postępowania)
- najczęściej używana metoda enableDefaults
- wykrywane są wszystkie operacje w sieci i local storage w głównym wątku
- logowana jest informacja
Tanie urządzenia z Android używają systemu plików free flash, który pozwala tylko na pojedynczą operację na pliku w zadanym czasie.
Zapis do pliku
FileOutputStream outStream = openOutStream(“file.dat”);
simpleWrite(outStream, “Xxx”);
closeOutStream(outStream);
protected FileOutputStream openOutStream(String filename) {
FileOutputStream outStream = null;
try {
outStream = openFileOutput(filename, MODE_PRIVATE);
}
catch (IOException e) {
}
return outStream;
}
protected void closeOutStream(FileOutputStream outStream) {
try {
if (outStream != null)
outStream.close();
} catch (IOException e) {
}
}
protected void simpleWrite(FileOutputStream outStream, String buffer) {
try {
outStream.write(buffer.getBytes());
} catch (IOException e) {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
…
StrictMode.enableDefaults();
}
AsyncTask
Wspiera pracę w tle i interakcję z UI
- najłatwiejszy sposób na przesunięcie pracy wpływającej na UI z głównego wątku
- enkapsuluje bardzo popularny wzorzec pracy w tle
- inicjuje pracę z wątku UI
- wykonuje pracę w wątku w tle
- podczas wykonywania okresowo aktualizuje UI w wątku UI
- po zakończeniu pracy aktualizuje UI w wątku UI
- dla stosunkowo krótkich operacji (kilka sekund lub mniej)
Jest to klasa abstrakcyjna
- dostarcza trzy główne metody do nadpisania
- doInBackground - wykonuje długie zadanie (konieczność nadpisania)
- onProgressUpdate - dostarcza informacje o postępie dla usera
- onPostExecute – dostarcza informacje dla użytkownika po zakończeniu
Jest to klasa generyczna z trzema typami parametrów
- pierwszy - dane przekazywane do doInBackground (tablica wartości o zmiennej liczbie elementów)
- drugi - dane przekazywane do onProgressUpdate (tablica wartości o zmiennej liczbie elementów)
- trzeci - dane przekazywane do onPostExecute (pojedyncza wartość)
- używamy typu Void dla nieużywanych typów parametrów
class MyWorker extends AsyncTask<Type1, Type2, Type3> { … }
Type1 foo1, foo2;
MyWorker m = new MyWorker();
m.execute(foo1, foo2); //automatycznie wywołuje doInBackground i przekazuje do niej otrzymane parametry
void onProgressUpdate(Type2… values) {
}
void onPostExecute(Type3 value) {
}
Type3 doInBackground(Type1 … values) {
…
Type2 bar1, bar2;
publishProgress(bar1, bar2);
…
}
Asynchroniczny zapis pliku
final ProgressBar pb = (ProgressBar) findViewById(R.id.progressBarMain);
initializeProgressBar(pb);
displayStartedMessage();
new AsyncTask<String, Integer, Void>() {
@Override
protected Void doInBackground(String… strings) {
String outputValue = strings[0];
FileOutputStream outStream = openOutStream(“xxx.dat”);
for (int i = 0; i < 10, i++) {
slowWrite(outStream, outputValue);
publishProgress(i);
}
closeOutStream(outStream);
return null;
}
@Override
protected void onProgressUpdate(Integer… values) {
pb.setProgress(values[0]);
}
@Override
protected void onPostExecute(Void aVoid) {
displayCompletionMessage();
cleanupProgressBar(pb);
}
}.execute(messageToWrite);
Wątki
Klasa Thread
- tworzy oddzielny wątek
- akceptuje referencję do interfejsu Runnable (implementacja interfejsu wykonuje pracę w metodzie run)
- wykonuje metodę start instancji Thread przy początku wykonywania
public class MyWorker implements Runnable {
void run() {
}
}
MyWorker worker = new MyWorker();
Thread t = new Thread(worker);
t.start();
Należy zachować ostrożność z wątkami w aktywnościach
- dodatkowe wątki nie zmienią cyklu życia Activity
- powinniśmy wyczyścić wszystkie wątki, kiedy user opuszcza
- w większości przypadków najlepszym wyborem jest onPause
- onDestroy jest ostatnią szansą na uniknięcie nagłego zabicia
- dla większości długo-wykonujących się tasków niezwiązanych z UI powinniśmy stosować serwisy
Loopers & handlers
- Android podobnie jak Windows używa pętli message
- do rozsyłania wiadomości do danego wątku
- cała praca UI i większość housekeeping-u zachodzi na message loop głównego wątku
- możliwość dodania dodatkowych wątków z pętlami message (często wykorzystywane przez systemowe serwisy)
- Looper w HandlerThread
Monitorowanie lokalizacji, wysyłanie do handlera, wykonywanie w handlerze
public class MainActivity extends Activity {
…
HandlerThread mHandlerThread;
Handler mHandler;
LocationListener mLocationListener;
…
private void btnCallRunnableOnHandlerOnClick(Button view) {
Handler handler = getMyHandler();
final long callingThreadId = Thread.currentThread().getId();
handler.post(new Runnable() {
@Override
public void run() {
long runningThreadId = Thread.currentThread().getId();
String msgText = “…”;
…
}
});
}
private void btnStartLocationMonitoringOnClick(Button view) {
LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
mLocationListener = new MyLocationListener();
HandlerThread handlerThread = getHandlerThread();
Looper looper = handlerThread.getLooper();
lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER /*provider*/, 10000 /*min czas*/,
0.0f /* min odległość*/, mLocationListener, looper); //opcjonalnie: looper
}
private void btnStopLocationMonitoringOnClick(Button view) {
stopLocationMonitoring();
}
private void stopLocationMonitoring() {
if (mLocationListener != null) {
LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
lm.removeUpdates(mLocationListener);
mLocationListener = null;
}
}
private void btnSendMessageToHandlerOnClick(Button view) {
Handler handler = getMyHandler();
long threadId = Thread.currentThread().getId();
…
Message msg = handler.obtainMessage(0, messageText);
msg.sendToTarget();
}
private HandlerThread getHandlerThread() {
if (mHandlerThread == null) {
mHandlerThread = new HandlerThread(“HandlerThread”);
mHandlerThread.start();
}
return mHandlerThread;
}
private Handler getMyHandler() {
if (mHandler == null) {
HandlerThread handlerThread = getHandlerThread();
mHandler = new MyHandler(handlerThread.getLooper());
}
return mHandler;
}
@Override
protected void onPause() {
super.onPause();
if (mHandlerThread != null) {
stopLocationMonitoring();
mHandlerThread.quit();
mHandlerThread = null;
}
}
}
public class MyHandler extends Handler {
public MyHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
String messageText = (String) msg.obj;
long threadId = Thread.currentThread().getId();
…
}
}
Serwisy cz.1
Serwisy w tle
- niewiele mniejszy priorytet niż bieżąca aktywność użytkownika
- komponent w aplikacji, nie osobna aplikacja
Implementacja
- dziedziczymy po klasie Service
- klasa Service rozszerza klasę Context (dostęp do większości metod Context-u tak jak Activity)
- wymagana implementacja onBind (często zwraca null)
- serwis ma osobny cykl życia
- onCreate - kiedy serwis jest tworzony
- onDestroy - przy zamykaniu
- onStartCommand - przy każdym Intent przekazanym do serwisu
- w manifeście lista z elementem <service>
public class MyService extends Service {
public IBinder onBind() { return null; }
public void onCreate() { … }
public void onDestroy() { … }
public void onStartCommand(…) { … }
}
<service
android:name=”.MyService”>
…
</service>
Android Studio
- New Android Component –> Service (tworzona jest klasa i wpis w manifeście)
public class MyService extends Service {
HandlerThread mHandlerThread;
Handler mHandler;
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
mHandlerThread = new HandlerThread(“MyService”);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
Intent intent = (Intent) msg.obj;
doWork(intent);
}
};
}
@Override
public void onDestroy() {
mHandlerThread.quit();
mHandlerThread = null;
mHandler = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Message msg = mHandler.obtainMessage();
msg.obj = intent;
msg.sendToTarget();
return 0;
}
private void doWork(Intent intent) {
String messageText = intent.getStringExtra(“MessageText”);
FileOutputStream outStream = FileHelper.openOutStream(this, “xxx.dat”);
for (int i = 0; i < 10, i++) {
FileHelper.slowWrite(outStream, messageText);
}
FileHelper.closeOutStream(outStream);
}
}
public class MainActivity extends Activity {
…
private void btnDoLongRunningWorkOnClick(Button button) {
String messageText = “abcd”;
Intent intent = new Intent(this, MyService.class);
intent.putExtra(“MessageText”, messageText);
startService(intent);
}
}
Aktualny proces: android.os.Process.myPid()
Aktualny wątek: Thread.currentThread().getId();
Brak komentarzy:
Prześlij komentarz