środa, 3 września 2014

Pojedynek z Androidem - odc.4 wielowątkowość, praca w tle, serwisy cz.1

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

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: