sobota, 11 października 2014

Pojedynek z Androidem - odc. 13 procesy i wątki

Poziom podstawowy i średni w miarę za nami, pora spróbować wznieść się trochę na ten wyższy. W ramach tego udało mi się dotrzeć do kilku istotnych, a zarazem ciekawych zagadnień z anatomii systemowej, których może nie każdy jest świadomy.

Sama idea piaskownicy dla aplikacji nie jest jakaś oryginalna, ot izolację aplikacji o unikalnej tożsamości jako izolowanego procesu i systemu plików spotykamy także w rodzinie Windows. W Android mamy jednak więcej jawnych form komunikacji między procesami. Tak porównując to Intent i Messenger w Windows/Windows Phone próbowałbym nazwać kontraktem, funkcjonalnością, taskiem. ContentResolver?  Źródła danych w ekosystemie Windows mamy, ale są to raczej bazy plikowe per aplikacja nie działające w osobnych procesach (chyba że myślimy o jakichś usługach systemowych dostarczających informacji,  które mogą czasami działać w ramach innego procesu). Binder?  Niskopoziomowych form komunikacji międzyprocesowej pomiędzy aplikacjami Windows Store raczej nie używamy. 

Dzielenie usera pomiędzy app-kami Android daje nam niezłą furtkę. Definiując w manifestach obu aplikacji ten sam identyfikator użytkownika i podpisując je tym samym certyfikatem możemy sprawić, że jedna aplikacja może odczytywać pliki drugiej aplikacji, jej zasoby czy współdzielone ustawienia.

Dzielenie procesu między app-kami Android zakłada, że dzielimy się userami. Dodatkowo tylko manifestach interesujących nas aplikacji ustawiamy ten sam identyfikator procesu. Jak coś będzie w jednym procesie to będzie wydajniej, w ContentResolver i IBinder znika narzut komunikacji międzyprocesowej.  Autor filmu pokazuje korzystanie z serwisu jednej aplikacji przez drugą (generowanie stub-ów z *.aidl kojarzy mi się z dawnymi zajęciami na studiach, gdzie były generowane stuby do Corby). Co prawda możemy to czynić bez sharowania usera czy procesu, ale możemy też doprowadzić, że wszystko będzie się nam wykonywało w jednym procesie!   W Windows\Windows Phone nie mamy możliwości użytkowania serwisu (czy raczej background task) przez różne aplikacje.  Komponenty też do niedawna mogły być używane tylko przez każdą aplikację mobilną Windows osobno, ale w Windows 8.1 pojawiła się pewna furtka dla aplikacji Enterprise tzw. Brokered Windows Runtime Components (przy czym jest to komunikacja między różnymi procesami). W Windows 8.1 aplikacje Enterprise mogą też wykorzystywać tzw. network loopback do komunikacji z lokalnymi serwisami.

Obsługa wielowątkowości, znów dużo możliwości, do których w Windows znajdzie się kilka odpowiedników. Jeśli chodzi o komunikat, że “aplikacja nie odpowiada”, to w mobilnym Windows\Windows Phone akurat czegoś takiego nie mamy. Całe WinRT API jest asynchroniczne, więc stosunkowo rzadko musimy ręcznie coś samemu pakować do innych wątków, bo coś nam przytnie UI. Czegoś na kształt loopera czy nawet handler threada w wysokopoziomowym API Windows nie kojarzę, są pętle z message, ale w kodzie niezarządzanym. Obsługa kodu w handlerach czasami może być wywoływana w Windows z poziomu innych wątków, ale nie ma z tym problemu (najwyżej modyfikacje na UI wywołujemy wtedy w tzw. dispatcherze). AsyncTask Androida to nieco mniej doskonały odpowiednik async z WinRT/.NET, o czym już kiedyś pisałem.  Pula wątków?  Ha, może zaczynam rozumieć po co na sprzęcie z Androidem są wprowadzane tak szybko procesory wielordzeniowe…

Mała wzmianka o kodzie natywnym w Android. Jest dedykowane do tego specjalne SDK i całe aktywności mogą być w pełni natywne. Oczywiście możemy też z poziomu języka Java wywoływać kod natywny przez JNI.  Samo JNI pozwoliłem sobie użyć kiedyś na studiach, kiedy kierowany wygodą napisałem cały program w Javie (C# nie był dozwolony), który wywołuję bibliotekę w C. Było to znacznie bardziej toporne od integracji C# 1.0 z kodem natywnym, nie wspominając o dzisiejszym niemal przezroczystym WinRT. Ciekawe na ile Java z JNI poszła do przodu? Jakie są ograniczenia z korzystania kodu natywnego na Android? Co możemy wywołać?  Szkoda, że autor filmu ograniczył się jedynie do ogólników, bo rozwinięcie tej tematyki mogło być bardzo ciekawe.

 

Intro

“Sandbox”

  • unikalna tożsamość
    • mapowanie aplikacji na user id
    • trochę różne w Android 4.2
  • unikalny proces (Dalvik, odpowiednik JVM)
  • unikalny system plików / zasobów

Komunikacja międzyprocesowa

  • IPC z linuksa/unixa nie pasuje do Androida
  • sieciowe sockety - możliwa, ale mało wydajna
  • udogodnienia Android
    • Intent
      • forma IPC
    • Messenger
      • obiekty przesyłane między procesami
      • referencja na handler między procesami
    • ContentResolver
      • IPC ukierunkowane na storage
      • używany z ContentProvider
      • standardowe operacje na SQL: Insert, Delete, Query
      • wsparcie dla wywoływania metod typowych dla danego content providera
    • IBinder & Binder
      • wysoka wydajność
      • podstawowy mechanizm IPC w Android, inne mechanizmy IPC wykorzystują go
      • wykorzystuje specjalne właściwości jądra Android
        • współdzielona pamięć pomiędzy procesami
      • używa specjalnie serializowanego formatu danych
        • wspiera każdy obiekt implementujący Parceable
      • może używać własnych interfejsów i metod
        • używane przez serwisy z wewnętrznego frameworku
        • wywołania metod pomiędzy procesami
        • definicja przy użyciu AIDL (Android Interface Definition Language)

Eclipse

  • dwa projekty z aktywnościami
  • menu kontekstowe na projekcie Debug As –> Debug Configurations… –> zakładka Remote Java Application (konfigacja portów)
  • możliwość debugowania za jednym razem aktywności wywołującej i wywoływanej

 

Dzielenie procesów i user id

Prawie unikalne User ID

  • wiązane z aplikacją w czasie instalacji
  • ustawienie w manifeście  <manifest … android:sharedUserId=”xxx.yyy.sharedId”
  • współdzielone ID jest stringiem (nie jest prawdziwym ID użytkownika)
  • aplikacje muszą być podpisane tym samym certyfikatem

Plusy i minusy współdzielonego user ID

  • Plus - dostęp do danych innej aplikacji
    • pliki
    • zasoby
    • współdzielone ustawienia
  • Minusy
    • nieefektywny RAM
    • trudny dostęp do danych innej aplikacji

Dostęp do danych innej aplikacji

public class XActivity extends Activity {

         private PackageManager mPM;

         …

         @Override

          public void onCreate(Bundle savedInstanceState)  {

                   …

                   mPM = getPackageManager();

          }

          @Override

          public void onStart()  {

                   …

                   try {

                           ApplicationInfo ai = mPM.getApplicationInfo(“com.xxx.yyy.SharedApp”);

                           string path = ai.dataDir + “/files”;

                           File inFile = new File(path, “data.txt”);

                           FileInputStream in = new FileInputStream(inFile);

                           int len = (int) inFile.length();

                           …

                           byte[] dataBytes = new byte[len];

                           in.read(dataBytes, 0, len);

                           in.close();

                           string data = new String(dataBytes);                          

                   } catch {

                   }                  

          }

}

Jeśli aplikacja 1 i 2 mają taki sam sharedUser w swoich manifestach, to aplikacja 2 może odczytać plik z wewnętrznego storage aplikacji 1.

Współdzielony proces

  • Definiujemy nazwę procesu w manifeście w tagu <application  … android:process=”xxx.yyy.sharedProcess”
    • tylko referencja (jak przy shared user)
    • odrobina semantyki
      • rozpoczynanie od ‘:’ dla prywatnego procesu w aplikacji
      • rozpoczynanie od małej litery dla globalnego (współdzielonego)
  • Musimy współdzielić user ID !
  • Współdzielenie procesu, współdzielenie zasobów
  • Większa wydajność - redukcja IPC dla własnych komponentów takich jak
    • IBinder
    • ContentProvider (dla wystawienia storage schema globalnie)

Wspólny serwis (przykład)

<service … android:exported=”true”

 

IShareService.aidl

 

interface IShareService  {

        boolean isSameProcess(int clientPid);

}

 

SampleShareService.java

public class SampleShareService extends Service  {

          …

          private ShareServiceImpl mBinder = new ShareServiceImpl();

          public IBinder onBind(Intent intent)  {

                   return mBinder;

          } 

          …

          private class ShareServiceImpl extends IShareService.Stub  {

                   public boolean isSameProcess(int clientPid)  {

                            return clientPid == Process.myPid();

                   }

          }  

}

IShareService.Stub - autogenerowana klasa na podstawie *.aidl  (tutaj: IShareService.java)

Aktywność “kliencka” - nie mamy w manifeście shared user id ani shared process

public class Client1Activity extends Activity  {

         …

         private IShareService mService;

         private ShareServiceConnection mConn;

         @Override

         public void onCreate(Bundle savedInstanceState)  {

                  …

                  mConn = new ShareServiceConnection();

                  Intent intent = new Intent(Intent.ACTION_MAIN);

                  intent.setClassName(“com.xxx.yyy.ShareService”, “com.xxx.yyy.ShareService.ShareService.SampleShareService”);

                  if (!bindService(intent, mConn, BIND_AUTO_CREATE))  {

                           …

                  }

         }

         …

         @Override

          public void onDestroy()  {

                   if (mConn != null) {

                           unbindService(mConn);

                           mConn = null;

                   }

                   super.onDestroy();

          }

          private class ShareServiceConnection implements ServiceConnection {

                   public void onServiceConnected(ComponentName name, IBinder binder) {

                            mService = IShareService.Stub.asInterface(binder);

                            try {

                                    if (mService.isSameProcess(Process.myPid()))  {

                                            …

                                    }

                            }

                   }

                   public void onServiceDisconnected(ComponentName name) {

                            mService = null;

                   }

          }

}

 

Wątki

Wielowątkowość

  • główny wątek UI
  • pula wątków używana przez framework

Wątek UI

  • Activity
  • Service
  • BroadcastReceiver

ANR: Application Not Responding

  • brak odpowiedzi na reakcję użytkownika po zadanym czasie
  • BroadcastReceiver nie może zakończyć działania po zadanym czasie
  • błąd krytyczny
  • nowsze wersje Android pozwalają ukrywać niektóre z tych komunikatów

Thread

Looper/Handler

  • Looper powiązany jest z konkretną instancją Thread
  • Looper czeka na obiekty Message
  • Looper dostarcza obiekty Message do Handler’a
  • rozszerzamy Handler lub dostarczamy Handler.Callback
  • używane dla dobrze zdefiniowanych interfejsów
    • zawartości Message
    • możliwość wykonywania obiektów Runnable w Handler

public class XActivity extends Activity implements Handler.Callback  {

          …

          private GenerateReceiver mgenRec;

          private DataGenThread mDGThread;

          protected Handler mHandler;

          …

          @Override

          public void onCreate(Bundle savedInstanceState)  {

                   …

                   mGenRec = new GenerateReceiver();

 

                   mHandler = new Handler(this);

                   mDGThread = new DataGenThread();

                   mDGThread.start();

          }

          @Override

          public void onStart() {

                   …

                  registerReceiver(mGenRec, new IntentFilter(“xxx”));

          }

          @Override

          public void onPause() {

                  …

                  unregisterReceiver(mGenRec);

          }

          @Override

          public void onDestroy() {

                  mDGThread.mWorkerHandler.obtainMessage(5);

                  try {

                          mDGThread.join();

                  }  catch (InterruptedException e)  {

                  }

                  …                 

          }

          @Override

          public void onClick(View v)  {

                  Message startMsg = mDGThread.mWorkerHandler.obtainMessage(1);

                  startMsg.sendToTarget();

          }      

          private class GenerateReceiver extends BroadcastReceiver  {

                  @Override

                  public void onReceive(Context context, Intent intent)  {

                          …

                  }

          } 

          @Override

          public boolean handleMessage(Message msg)  {

                  …

                  return false;

          }

          private class DataGenThread extends Thread implements Handler.Callback  {

                   private Looper mWorkerLooper;

                   protected Handler mWorkerHandler;

                   @Override

                   public void run()  {

                           Looper.prepare();

                           mWorkerLooper = Looper.myLooper();

                           mWorkerHandler = new Handler(mWorkerLooper, this);

                           Looper.loop();

                   }

                   @Override

                   public boolean handleMessage(Message msg)  {

                          if (…)  {

                                  mWorkerLooper.quit();

                          }

                           return false;

                   }

          }

}

HandlerThread

  • uproszczona wersja Thread/Looper/Handler
  • automatycznie tworzy Thread i Looper
  • możemy przechwycić moment inicjalizacji (onLooperPrepared)
  • pojedyncze wywołanie, może długo się wykonywać

private class DataGridThread extends HandlerThread implements Handler.Callback  {

        protected Handler mWorkerHandler;

        …

        @Override

        protected void onLooperPrepared()  {

                mWorkerHandler = new Handler(getLooper(), this);

        }

        @Override

        public boolean handleMessage(Message msg) {

                …

                if (…)

                        getLooper().quit();

                return true;

        }

}

AsyncTask

  • raczej do stosunkowo krótkich operacji (tj. kilka sekund)
  • ograniczenia na tworzenie i wykonywanie
  • operacje zmieniały się razem z wersjami Android
    • na początku sekwencyjne wykonywanie w wątku w tle
    • Donut (API 4) używał równoległego
    • Honeycomb (od API 11) powrócił do sekwencyjnego
      • możliwość nadpisania
  • tylko pojedyncze wywołanie
  • łatwe wycieki

Anulowanie taska przy niszczeniu aktywności

task.cancel(true);

try  {

       task.get();

}  catch (Exception e)  {

}

W środku doInBackground możemy sprawdzić, czy ktoś “nas” nie anulował przez metodę isCancelled()

Pule wątków

  • Rozszerzalny framework
    • długo wykonujące się taski nie wymagające interakcji z UI
    • operacje równoległe
    • wydajne używanie wątków przez reużytkowanie
  • Różne polityki dla anulowania, odrzucenia
  • Rozmiar puli

public class XActivity extends Activity implements Handler.Callback  {

       …

       private ExecutorService mExec;

       @Override

       public void onCreate(Bundle savedInstanceState)  {

              …

              //na nowych urządzeniach ilość rdzeni

              int numProcessors = Runtime.getRuntime().availableProcessors();

              mExec = Executors.newFixedThreadPool(numProcessors);

       }

       …

       @Override

       public void onDestroy()  {

              mExec.shutdown();

              try  {

                      if (!mExec.awaitTermination(60, TimeUnit.SECONDS))  {

                              mExec.shutdownNow();

                              mExec.awaitTermination(60,  TimeUnit.SECONDS);

                      }

              }   catch (InterruptedException e)  {

                      mExec.shutdownNow();

                      Thread.currentThread().interrupt();

              }

              …

       }

       …

       private void scheduleGeneration()  {

              mHandler.obtainMessage(1).sendToTarget();

              for (int i = 0; i < 10; i++)  {

                      DataGenRunnable curGen = new DataGenRunnable(i);

                      if (mExec.submit(curGen)  == null)  {

                               try  {

                                        Thread.sleep(1);

                                }  catch (…) {

                                }

                      }

              }

       }

       …

       private class DataGenRunnable implements Runnable  {

              …

              @Override

               public void run()  {

                      …

                      mHandler.obtainMessage(2).sendToTarget();

               }

       }

}

Natywne wątki

  • Natywność?
    • C/C++ dostępny przez Java
    • od API 9 (GingerBread) możliwość pisania natywnych aktywności (i aplikacji)
    • wymagane Android NDK
    • wymagane JNI (dostępność kodu natywnego z poziomu Java)
  • Dwie opcje używania wątków do natywnego kodu
    • wątki Java, dostęp do natywnych metod (wydajność ?)
    • natywne pthreads pod spodem  (trudność w zarządzaniu)
  • Duża elastyczność, wysoka wydajność
  • Brak wbudowanej we framework koordynacji

Brak komentarzy: