|
O midletach dokładniej, czyli specyfika pisania programów na telefony komórkowe Przesłanki, czyli dlaczego tak, a nie inaczej Tworzenie aplikacji mających działać na urządzeniach przenośnych - telefonach komórkowych czy też nawet palmtopach wymaga nieco innego podejścia niż pisanie programów na pecety. Należy zdawać sobie sprawę z dwóch czynników określających specyfikę urządzeń przenośnych. Pierwszy z nich to urządzenia same w sobie. Przede wszystkim mają one inne możliwości. Procesor takiego urządzenia jest wiele wolniejszy od procesorów PC, ograniczenia na wykorzystanie pamięci też są znacznie bardziej restrykcyjne. Innym aspektem jest przenoszalność aplikacji. Java umożliwia pisanie programów całkowicie przenośnych. Interfejs programisty jest dobrze opisany i wspólny dla wszystkich urządzeń obsługujących Javę, wiele aspektów działania urządzeń przenośnych nie zostało w nim zawartych. Aspektów ważnych tak dla programisty, jak i zwykłego użytkownika takich aplikacji, jak choćby wyświetlacz. Poszczególne telefony różnią się wymiarami ekranu i mimo iż można teoretycznie napisać aplikację która będzie wyświetlana bez konieczności przesuwania obrazu na wszystkich urządzeniach to jednak program taki nie będzie wyglądał estetycznie. Drugi z czynników jakie należy uwzględnić pisząc program na "komórkę" to użytkownik. Użytkownik programu na peceta jest tolerancyjny. Kilkanaście lat obecności tych urządzeń na rynku "wychowało" odbiorców aplikacji. Nikogo już nie dziwią zawieszenia się komputera, lub też długie pozorne przerwy w pracy, kiedy maszyna "myśli". Rzecz ma się zupełnie inaczej w przypadku użytkowników telefonów. Mogą to być osoby nie mające żadnej styczności z komputerem, mało tego, komórka w żaden sposób nie przypomina peceta, więc nie można oczekiwać od nich tolerancji dla powolnego, lub niestabilnego działania programu. Te właśnie dwa czynniki określają podejście programisty do pisania midletów. Przede wszystkim midlet musi działać szybko. Oznacza to takie napisanie kodu, aby był jak najmniejszy efektywnie wykorzystywał możliwości urządzenia. Bardzo często szybkość działania programu zależy od czynników zewnętrznych - jak np. połączenia sieciowe. Uwzględniając możliwe "przestoje" aplikacji należy zadbać o odpowiednie sygnalizowanie, że program jeszcze "żyje" - zegarek, klepsydra czy zwykły pasek postępu są jak najbardziej a propos. Zdrowy rozsądek oraz odpowiednie planowanie i projektowanie aplikacji dają wiele, poniższe wskazówki pozwolą usystematyzować to, co dobry programista robi intuicyjnie. Na koniec zaprezentowana zostanie prosta aplikacja mogąca stanowić pewne udogodnienie w testowaniu i debugowaniu programów w warunkach "komórkowych", tj. wtedy gdy odpalamy program w środowisku gdzie nie ma konsoli tekstowej ani standardowego wyjścia błędów. Ponieważ aplikacje takie mogą być tworzone na różne platformy - telefony, palmtopy i inne tego rodzaju urządzenia, zaś omawiane zagadnienia są w dużej mierze niezależne od niej, w dalszej części tego artykułu będę używał sformułowania urządzenie przenośne na określenie tych wszelkich urządzeń tego typu, czyli telefonów, palmtopów itd. Na początek przygotujemy środowisko programistyczne. Aby skompilować program napisany, dajmy na to w notatniku (JBuilderze, Kawie, czy każdym innym edytorze), do działania na urządzeniu przenośnym potrzebujemy bibliotek J2ME. Nie są one dostarczane razem ze standardowym SDK, należy je ściągnąć oddzielnie (odpowiednie adresy na końcu artykułu). Odpowiednie aplikacje udostępnia SUN. Nas interesuje J2ME Wireless Toolkit. Plik J2ME_WIRELESS_TOOLKIT.EXE możemy ściągnąć z jej strony. Po zainstalowaniu i uruchomieniu ukazuje się okno :
Jak widać program prosi o utworzenie nowego, lub otworzenie istniejącego projektu. Jeżeli tworzymy nowy projekt należy wprowadzić nazwę projektu i nazwę głównej klasy:
Po wprowadzeniu żądanych nazw pojawia się z kolei okno definicji pliku deskryptora midletu:
Plik deskryptora zawiera informacje określające midlet. Jest to zorganizowane w postaci par atrybut - wartość. Są one podzielone na zbiory: wymagane (required), opcjonalne (optional), zdefiniowane przez użytkownika (user-defined) oraz parametry midletu. Parametry wymagane to : MIDlet-Name - nazwa midletu Niektóre parametry opcjonalne MIDlet-Description : opis słowny midletu Midlety są identyfikowane w sieci na podstawie ich deskryptorów. Wyszukiwania midletów (plików .JAD) dokonuje się przy użyciu dostępnych metod - czyli po prostu surfując po sieci. Kiedy już natrafimy na interesujący nas midlet i obejrzywszy plik deskryptora stwierdzimy że chcemy go mieć wybieramy opcję ściągania midletu w telefonie komórkowym. Resztą zajmuje się system operacyjny telefonu. Teraz kilka słów o strukturze katalogów WTK. W katalogu w którym zainstalowane zostało WTK (nazwijmy go $WTKDIR) stworzony został katalog apps. Zawiera on wszystkie projekty utworzone w WTK. Także przed chwilą stworzony projekt Doggz :
Struktura każdego projektu jest prosta - katalog projektu zawiera następujące
podkatalogi : Możliwe jest uruchamianie napisanych programów w środowisku WTK (przycisk run). WTK udostępnia kilka standardowych, prostych emulatorów, różniących się parametrami (wyświetlacz, kolory). Możliwe jest także rozszerzenie listy dostępnych w emulatorów przez skopiowanie emulatora "zewnętrznego" do katalogu $WTKDIR/wtklib/devices. W ten sposób możliwe będzie kompilowanie programów przy wykorzystaniu specyficznych dla danego telefonu bibliotek (emulatory dostarczane z WTK są emulatorami zawierającymi tylko podstawowe biblioteki - pakiet J2ME) Możliwe jest także rozbudowanie WTK o obfuscatora. W tym celu należy plik .jar zawierający to narzędzie skopiować do katalogu $WTKDIR/bin. Kiedy już mamy WTK i wiemy jak je rozbudowywać do pracy potrzebny nam
jest emulator. Tak jak WTK należy go ściągnąć ze strony producenta (adresy
na końcu artykułu). Niektóre emulatory można skopiować do katalogu $WTKDIR
(rozszerzając w ten sposób funkcjonalność WTK), część jednak może odmówić
współpracy z WTK. Wszystkie emulatory wszelako mogą pracować jako oddzielne
aplikacje, wczytujące midlety i wyświetlające wyniki ich działania.
Po wybraniu opcji uruchomienia programu (albo przyciskiem run w WTK, albo uruchamiając emulator oddzielnie) pojawia okno emulatora, w przypadku emulatora Nokii 3410 wygląda to następująco:
Za pomocą menu file/open można wczytać midlet. Kiedy już mamy poprawnie działający program należałoby go ściągnąć na komórkę. Można go w tym celu umieścić w Internecie i odnajdując plik deskryptora w sieci za pomocą WAPu ściągnąć go na nasz telefon. Innym rozwiązaniem jest przesłanie midletu z komputera na komórkę za pomocą kabla. Przewód do łączenia peceta z komórką, wraz z odpowiednim oprogramowaniem jest dostarczany z razem z telefonem. Można go też kupić oddzielnie w większości sklepów z akcesoriami do telefonów komórkowych. Odpowiednie oprogramowanie (służące nie tylko do transferu midletów, lecz także np. do wgrywania nowych melodyjek, czy ekranów powitalnych) można ściągnąć z Internetu, ze strony producenta telefonu. Tak więc zwykłe postępowanie przy tworzeniu midletów jest następujące
: Kiedy już mamy środowisko programistyczne, mamy pomysł na program i wiemy jak go napisać by działał poprawnie przydatną wiedzą jest jak go napisać by działał efektywnie (czytaj szybko). Dalsza część artykułu prezentuje podstawowe techniki pisania aplikacji na urządzenia przenośne. Dziel się problemami z innymi Przede wszystkim nie wszystkie operacje trzeba wykonywać w telefonie. Strategia jest prosta - jeżeli twoja aplikacja jest dostatecznie duża, i wymaga komunikacji z serwerami sieciowymi, być może dobrym rozwiązaniem będzie umieszczenie części jej logiki na serwerze. Unikniesz w ten sposób możliwego obciążania procesora urządzenia przenośnego i co za tym idzie niepotrzebnych, a denerwujących użytkownika "zawieszeń" systemu. Odpowiedź na pytanie jak podzielić aplikację, ile obliczeń wykonywać w urządzeniu a ile na serwerze nie jest prosta. Należy rozpatrywać konkretne przypadki. Poza mocami obliczeniowymi urządzenia należy wziąć pod uwagę także dostępne pasmo transmisyjne (wyniki obliczeń trzeba przesłać z serwera), a także jego koszt (zarówno finansowy jak i czasowy).Niekonieczne też chcemy narażać poufność naszych danych przez umieszczanie ich na potencjalnie podatnym na włamania serwerze w sieci. Proste jest piękne Z racji ograniczenia pamięci przeznaczonej na przechowywanie midletów
rozmiar programu ma bardzo duże znaczenie. Mniejszy program ściąga się
szybciej i taniej, szybciej się instaluje, wreszcie szybciej się uruchamia.
Przyjętym standardem rozpowszechniania midletów jest ich pakowanie do
postaci pliku .JAR.
} //.. kod klasy } Zwykle duża część kodu aplikacji zajmuje się interakcją z użytkownikiem. Staraj się wykorzystywać ten kod wielokrotnie, gdzie się tylko da. Spowoduje to nie tylko zmniejszenie rozmiaru programu - podobny układ menu, podobna szata graficzna wyświetlanych komunikatów, wreszcie podobna obsługa pewnych jego części sprawi że użytkownik szybciej i łatwiej przyswoi sobie obsługę programu. Łatwość, intuicyjność obsługi i szybkość jej nauki będzie zaś dodatkowym plusem aplikacji. Bardzo przydatnym narzędziem do zmniejszania rozmiaru aplikacji jest obfuscator. Jego działanie polega na zamianie identyfikatorów i fragmentów kodu na krótsze, charakterystyczne sekwencje znaków. Wykrywa także nieużywane pola i metody prywatne klasy i usuwa je. Sprawia że reverse enginering skompilowanych klas jest praktycznie niemożliwy i, co chyba ważniejsze, zmniejsza, czasem nawet znacznie, rozmiar aplikacji. Adresy skąd można ściągnąć to narzędzie znajdują się na końcu artykułu. Obfuscator nie zajmuje się składowymi publicznymi (polami i metodami) ponieważ inne klasy mogą z nich korzystać. Można go jednak skonfigurować tak, aby traktował zbiór klas (pakietów), których używamy, jako zbiór zamknięty. Oznacza to, że zakładamy iż żadna inna klasa nie będzie korzystała ze zdefiniowanych w naszych pakietach składowych publicznych. To umożliwi obfuscatorowi zoptymalizowanie o wiele większej liczby linii kodu. Inną możliwością jest także nakazanie traktowanie każdej publicznej lub chronionej metody (pakietu) tak jak prywatnej czyli założenie że nikt z zewnątrz nie będzie się do nich odwoływał. Dobrą praktyką zatem jest deklarowanie samemu możliwie największej liczby składowych prywatnymi. Nie tylko ze względu na obfuscatora, po prostu im mniej publicznych składników, tym prościej optymalizować i debugować kod programu. Szanuj pamięć Możliwości urządzenia przenośnego są ograniczone. Ma ono dwa typy pamięci : pamięć dynamiczną, i schowek (persistent storage). Pamięć dynamiczną przechowuje dane związane z uruchomioną aplikacją - w szczególności jej stos i stertę, schowek zaś jest zabezpieczaną przed zapisem pamięcią trwałą. W konsekwencji dostęp do schowka jest dość wolny. Przykładowe wielkości pamięci na przykładzie Nokii (dokument "Characteristics of Nokia Java - Enabled Phones, ver. 1.0, 26 sept. 02" - dostępny na http://www.forum.nokia.com) ilustruje poniższa tabelka: Model Pamięć dla midletów Sterta Schowek (Persistent Storage) Ograniczenie
rozmiaru ściąganych midletów Jak widać pamięć dla midletów może różnić się dość znacznie - Nokia 7210 ma jej cztery razy więcej niż Nokia 3410. Nie oznacza to jednak iż program na Nokię 7210 może mieć rozmiar 600kB. Maksymalna wielkość programu do ściągnięcia na Nokię 7210 to ok. 64kB, o ok. 14kB więcej niż na Nokię 3410. Tak więc nie w możliwej wielkości midletów siła Nokii 7210, lecz w ich ilości. Brak danych na temat rozmiaru schowka oznacza że jest on wliczony w pamięć przeznaczoną na midlety (kolumna 2) Jak widać pamięci nie jest dużo. Efektywne nią zarządzanie pozwoli pisać
szybkie i efektywne programy. Java jest bardzo wygodnym językiem jeżeli
chodzi o zarządzanie pamięcią. Zwalnia programistę od kłopotliwego obsługiwania
tworzenia i usuwania obiektów, przechowywania referencji i zarządzania
nimi. Mimo iż odśmiecacz Javy jest pożyteczny, i bardzo upraszcza programowanie,
warto zdawać sobie sprawę ze specyfiki jego działania. Jeżeli zaalokowane
zostanie zbyt dużo obiektów w zbyt krótkim czasie, odśmiecacz może mieć
problemy z nadążaniem ze sprzątaniem. Spowoduje to "zawieszenie"
się aplikacji w oczekiwaniu na zakończenie sprzątania. Ponadto deklarując
tyle obiektów na raz zwiększona zostanie ilość chwilowo potrzebnej pamięci.
W końcu zostanie zwolniona, nie zostanie jednak zwrócona systemowi, mimo
iż większa jej część nie będzie już potrzebna aplikacji. Ilość pamięci
potrzebnej midletowi do działania jest określana na podstawie maksymalnej
ilości wymaganej pamięci, nie średniej. for (i=0;i<table.length();i++) //... jakieś obliczenia związane z obiektem score results[i]=score.getScore(); można zadeklarować obiekt obj poza pętlą: Score score = new Score(); for (i=0;i<table.length();i++) results[i]=score.getScore(); Owszem, wymaga to dopisania kilku linijek kodu (funkcja load()), ale może oszczędzić nieco pamięci, i czasu działania programu. Inną dobrą praktyką jest wspomaganie odśmiecacza przez nadawanie nieużywanym referencjom wartości null. Przykład : public class MyMidlet extends midlet public void function (Object o) someObj=null; // referencja nie jest nam już potrzebna Przyrównując referencje do null ułatwiamy odśmiecaczowi znajdowanie i usuwanie niepotrzebnych obiektów. Poza wspomaganiem usuwania obiektów z pamięci można zrobić mały wysiłek i zredukować ilość niepotrzebnie tworzonych obiektów. Każdy zgłoszony wyjątek powoduje utworzenie obiektu klasy dziedziczącej po klasie Exception (w zależności od tego jaki to wyjątek będzie to klasa IOException, RuntimeException..itd.) Aby temu zaradzić można spróbować obsługiwać błędy w sposób tradycyjny tam gdzie to tylko możliwe, zostawiając metodę rzucania wyjątków na naprawdę wyjątkowe sytuacje. Każdy obiekt jakiego używasz musi być trzymany w pamięci. Każdy zdefiniowany obiekt wpływa więc a wydajność twojego programu. Aby zredukować ilość deklarowanych obiektów zamiast nich można rozważyć stosowanie typów prostych jak int, czy boolean. Dla przykładu rozważmy dwie deklaracje funkcji z pakietu java.awt.Component: public void setSize(Dimension dim); public void setSize(int width, int height); Pierwsza z nich wymaga zaalokowania obiektu typu Dimension, zaś druga dwóch typów prostych. O ile drugi wymaga napisania kilku linijek kodu więcej to nie wymaga deklarowania obiektu. Poza tym, co ważniejsze, pierwsza z funkcji jest zdefiniowana następująco: public void setSize(Dimension dim) więc jej spowoduje wykonanie niemal identycznych operacji co przy wywoływaniu drugiej. Wywołanie drugiej wersji tej funkcji oszczędzi tworzenia obiektu potrzebnego do wywołania funkcji wywołującej drugą metodę. Unikanie alokowania niepotrzebnych obiektów, i alokowanie obiektów tylko wtedy kiedy są potrzebne jest dobrą praktyką. Kontrola potrzeby alokowania obiektu praktyce polega na sprawdzaniu, czy obiekt, który mamy zamiar inicjalizować nie został przypadkiem zainicjalizowany wcześniej. Takie sprawdzanie ma sens, ponieważ java zapewnia wstępną inicjalizację deklarowanych obiektów wartościami domyślnymi - w przypadku typów złożonych jest to wartość null. public class klasa private Vector vektor; } Bardzo pamięciożerną operacją jest łączenie łańcuchów znakowych. Każda operacja łączenia obiektów typu String powoduje utworzenie obiektu StringBuffer. Następnie wywoływana jest jego metoda append(), a następnie toString(). Szczególnie mało wydajne jest stosowanie tego w pętlach Tak więc poniższą funkcję public String concatenate(string a, string b, int times) można, a nawet należy zoptymalizować do postaci: W ten sposób niejako przejmujemy sprawy w swoje ręce - sami tworzymy StringBuffer, i sami go obsługujemy. Różnica jest tylko taka, że tworzymy go tylko raz, a nie times razy. Oszczędność czasu i pamięci oczywista. Powyższa optymalizacja ma sens nawet wtedy gdy łączymy tylko dwa łańcuchy. W przypadku pierwszym zawsze jest inicjowana zmienna c. W każdym obrocie pętli tworzony jest StringBuffer. Jeżeli dodajemy dwa stringi pętla obraca się tylko raz, więc StringBuffer tworzony jest też raz. W sumie tworzone są dwie zmienne : StringBuffer i String. W drugim przykładzie w takim przypadku zostanie utworzony tylko StringBuffer. Zarówno StringBuffer, jak i String, a także Vector i HashTable operują na tablicach. Są to po prostu obiekty opakowujące tablice. W przypadku dwóch pierwszych typów są to tablice znakowe, w przypadku pozostałych tablice dowolnego typu. Tak więc kiedy tworzony jest obiekt typu String, StringBuffer, Vector, lub HashTable tworzona jest zwykła tablica. Kiedy zaś z obiektu typu StringBuffer tworzysz obiekt typu String wskaźnik obiektu String wskaże na tablicę utworzoną w StringBuffer. Nie zawsze operowanie bezpośrednio na tablicach jest sensowne - zwykle korzyści są zbyt małe w stosunku do włożonego wysiłku (bardziej skomplikowany kod). Warto pamiętać o takiej możliwości poprawienia wydajności programu. Bądź szybki Używanie lokalnych zmiennych tak często jak to tylko możliwe pozwoli znacznie przyspieszyć działanie programu. W przypadku częstych odwołań do pola jakiejś klasy, rozsądnym wyjściem jest przypisanie wartości tego pola do zmiennej lokalnej. Pozwoli to zwykle co najmniej uniknąć wielokrotnych wywołań odpowiedniej metody zwracającej żądaną wartość. Podobnie z tablicami. Przy każdym odwołaniu do elementu tablicy java sprawdza między innymi czy indeks żądanego elementu nie wykracza poza rozmiar tablicy. Przypisanie wartości często używanego elementu tablicy do zmiennej lokalnej sprawi że sprawdzanie to zostanie wykonane tylko raz, co odrobinę przyśpieszy działanie programu. Oczywiście opisywane postępowanie ma sens tylko w przypadku zmiennych, których wartość się nie zmienia. W przeciwnym wypadku, jeżeli to ma sens, można rozważyć jakiś sposób aktualizacji zmiennej lokalnej po każdej aktualizacji zmiennej "źródłowej". Kolejną metodą przyspieszenia działania programu jest optymalizacja wykorzystania schowka (Persistent Storage). Ponieważ dostęp do niego jest dość powolny, nie warto czytać (i zapisywać) danych w nim zapisanych bajt po bajcie. Java SE udostępnia strumienie buforowane (BufferedReader, BufferedWriter itd.), Java ME niestety nie daje takich udogodnień - należy je stworzyć samemu. Nie jest to trudne - wystarczy funkcja buforująca dane przeznaczone do zapisywania w tablicy bajtowej, i zapisująca do schowka od razu całą tablicę. Dokładny sposób postępowania zawarty jest w przykładzie. Klasa Logger zapisuje w ten sposób dane do schowka. Za każdym razem, kiedy program próbuje alokować pamięć, powinien być przygotowany na porażkę. W przeciwnym wypadku system operacyjny urządzenia w takiej sytuacji po prostu przerwie jego działanie wyświetlając (albo i nie) nieelegancki komunikat. Będąc świadomym możliwości niepowodzenia przy alokacji pamięci aplikacja może w takiej sytuacji odpowiednio zareagować. Na przykład wyświetlić elegancko zakończyć pracę podając powód takiego postępowania użytkownikowi. Niby efekt taki sam, ale jego opinia o twoim programie będzie nieco bardziej pozytywna. Bardzo ważną sprawą są połączenia sieciowe. Należy je zamykać tak szybko, jak tylko skończy się je wykorzystywać. Rozważmy następujący kod: HttpConnection http = null; W przypadku wystąpienia wyjątku połączenie może nie zostać zamknięte. Sterowanie zostanie przekazane do obsługi wyjątku i mamy katastrofę. Poniższy przykład jest ilustracją rozwiązania tego problemu. Komendy zawarte w bloku finally zostaną wykonane zawsze, bez względu na to w jaki sposób zakończy się blok try. Jest to dobre miejsce na zwolnienie nieużywanych zasobów - nastąpi to nawet wtedy, gdy zostanie zgłoszony wyjątek w trakcie operowania na zasobach. Klauzula try w bloku finally zapewnia dodatkową obsługę błędów - w przypadku wystąpienia wyjątku w trakcie zamykania połączenia (w końcu tu też może nastąpić błąd..) można zareagować w żądany sposób, choćby wyświetlając odpowiedni komunikat. Ułatwia to debugowanie programu, nie wspominając nawet o biednym użytkowniku szukającym rozpaczliwie odpowiedzi na pytanie dlaczego jego ulubiona gra wiesza się przy próbie wysłania pobitego rekordu do serwera internetowego.... HttpConnection http = null; Przenośność, czyli rzeczywistość skrzeczy. Pisząc aplikacje na urządzenia przenośne bardzo często spotykamy się
z ich nieprzenośnością. Jest to związane z pewnym cechami indywidualnymi
poszczególnych urządzeń, jak chociażby rozmiarami wyświetlacza. O ile
klasyczne API jest dosyć dobrze opisane i zestandaryzowane, o tyle sprawa
ma się zupełnie inaczej kiedy dochodzimy do interfejsu użytkownika. Każde
urządzenie, czasem nawet w obrębie tej samej firmy (mogą się na przykład
różnić dostępnymi kolorami ekranu, czy też liczbą diod), jest nieco różne.
Tak więc mimo iż np. telefony Nokia mają podobne rozmiary wyświetlaczy,
to już telefony np. Alcatela mogą mieć te rozmiary zupełne inne. Nie wspominając
o stosowanych konwencjach obsługi urządzenia (układ menu, dodatkowe przyciski
z określonymi funkcjami, itd.). Każdy producent udostępnia inne metody
obsługi cech indywidualnych swojego telefonu. W obrębie jednej marki jest
to przeważnie jeden pakiet, który należy importować. Ale co z sytuacja,
kiedy chcemy aby nasza aplikacja działała i na telefonach dwóch różnych
producentów? Jest prostsze rozwiązanie niż tworzenie dwóch aplikacji.
Wystarczy oddzielić logikę aplikacji od interfejsu użytkownika. W ten
sposób napisanie różnych wersji tego samego programu na różne urządzenia
będzie mniej skomplikowane. class Logic UI ui; // interfejs użytkownika. public int getX() // zwraca współrzędną X pozycji bohatera public int getY() // zwraca współrzędną Y pozycji bohatera public void setX(int x) //ustawia współrzędną X pozycji bohatera public void setY(int x) //ustawia współrzędną Y pozycji bohatera
}
class UI //.. metody klasy public void changed() public void up() // wywołana na żądanie przesunięcia bohatera w górę Innym rozwiązaniem mogłoby być umieszczenie klasy UI w oddzielnym wątku. W pętli, co określony przedział czasu następowałoby sprawdzanie pozycji postaci i odświeżanie ekranu. Inna klasa, z kolei, należąca do modułu interfejsu użytkownika nasłuchiwałaby czy nie wciśnięty został klawisz, i w razie wystąpienia tego zdarzenia w odpowiedni sposób modyfikowałaby zmienne X i Y klasy Logic. Klasa UI oraz pozostałe klasy modułu interfejsu użytkownika są klasami specyficznymi dla danej platformy. Przy pisaniu programu w wersji na inne urządzenie należałoby napisać od nowa tylko te klasy, uwzględniając możliwości (w praktyce dostępne funkcje w pakietach konkretnych producentów) konkretnej platformy. Klasa logiki oraz pozostałe klasy tego modułu mogłyby pozostać w niezmienione dla wszystkich platform. Napisałeś? Sprawdź jak działa! Java daje duże możliwości do testowania wydajności programów, znajdowania
wąskich gardeł i podglądania wykorzystania pamięci. Mimo iż część z metod
wykorzystywanych do tych celów nie została zaimplementowana w J2ME, pozostało
kilka użytecznych i, na dobrą sprawę wystarczających, sposobów aby dowiedzieć
się ciekawych rzeczy na temat swojego programu. public long freeMemory() i pierwsza metoda zwraca ilość wolnej pamięci (w bajtach) środowiska uruchomieniowego aplikacji, druga rozmiar tegoż środowiska (też w bajtach). Co ciekawe, rozmiar pamięci może się zmieniać - system operacyjny urządzenia może przydzielić aplikacji (a w zasadzie jej środowisku) dodatkową pamięć. Działanie w/w funkcji ilustruje przykład: public void memoryTest() long c,b,a; System.gc(); // uruchomienie odśmiecacza c=runtime.totalMemory(); System.out.println("total memory:"+c+" string size:"+a-b+"
memory left:"+a); Tyle o pamięci. Teraz chcielibyśmy sprawdzić szybkość działania programu. Służy do tego metoda public static long currentTimeMillis() z pakietu java.lang.System : public void timeCounter() start=System.currentTimeMillis(); my_function(); // dowolna funkcja, której czas działania chcemy sprawdzić finish=System.currentTimeMillis(); System.out.println("egzecution time:"+finish-start); Przykład. Zajmiemy się teraz prostą aplikacją zapisującą logi systemowe. Klasy Logger i Level należą do pakietu java.util.logging J2SE w wersji 1.4. Ponieważ w J2ME nie ma żadnej klasy o podobnej funkcjonalności adaptujemy do naszych celów klasy z J2SE. W tym celu znacznie je uprościmy. Najpierw należy zdefiniować poziomy logowania. Służy do tego klasa Level.
Określa ona zbiór standardowych poziomów logowania. Włączenie logowania
na określonym poziomie włącza także logowanie na wszystkich wyższych poziomach.
Poziomy określone są przez nazwę i wartość liczbową. Są one następujące
: W pakiecie java.util.logging zdefiniowano więcej poziomów, nam wystarczy te sześć. package logger; public class Level private final int value; // wartość liczbowa poziomu
// i funkcje dostępu do pól obiektu. Następnie zdefiniujemy klasę Logger, dostosowując istniejącą klasę z pakietu java.util.logger do naszych potrzeb. package logger;
private Level level; // poziom logowania Zmienna level określa poziom logowania. Komunikaty o niższym poziomie
(mniej ważne) zostaną odrzucone (nie będą zapisane). Możliwe jest także
takie ustalenie poziomu logowania klasy Logger aby ni zostały zapisywane
żadne komunikaty (poziom OFF), bądź też były zapisywane wszystkie (poziom
ALL). Dwie kolejne zmienne typu boolean - zmienne logToStdOut i logToRS
określają miejsce zapisu logów. Jeżeli zmienna logToStdOut ma wartość
true logi będą wypisywane na standardowe wyjście systemu (zwykle na ekran).
Często jednak nie ma możliwości odczytu takich logów jako że część emulatorów
telefonów nie udostępnia terminalu tekstowego, nie wspominając już o uruchamianiu
programu na rzeczywistym urządzeniu. Przydatna wtedy jest możliwość zapisywania
logów do schowka. Takie zapisane dane można wtedy za pomocą innego midletu
(jednakże z tego samego midlet suite) odczytać i odpowiednio zinterpretować.
Ponieważ wyświetlacz telefonu jest zwykle zbyt mały by wygodnie odczytywać
logi, można je przesłać do serwera sieciowego, gdzie będzie je można dalej
przetwarzać, lub po prostu odczytać. Aby włączyć logowanie do schowka
zmienna logToRS musi mieć wartość true. Obie zmienne są ustalane niezależnie,
tzn. jeżeli obie mają wartość true logowanie nastąpi i na konsole i do
schowka. public Logger(Level level) // konstruktor Zdefiniowaliśmy dwa konstruktory, różniące się argumentami wywołania. Można samemu utworzyć obiekt typu Level, lub przekazując odpowiednie argumenty obiektowi Logger zlecić mu utworzenie takiego obiektu. W obydwu przypadkach powstanie dokładnie jeden obiekt Level (w przypadku przekazania go jako parametru konstruktora do obiektu Logger zostanie jedynie przypisana mu referencja). public static RecordStore openLog() throws RecordStoreException return rs;
Metoda clearLog() czyści log systemowy. Obywa się to przez wywołanie metody klasy RecordStore kasującej zbiór rekordów ze schowka. Parametrem tej metody jest nazwa zbioru rekordów. W przypadku nie znalezienia zbioru rekordów, lub jakiegokolwiek innego błędu związanego ze schowkiem metoda ta zwraca RecordStoreException public static void closeLog() Zamykanie logu odbywa się także przez wywołanie odpowiedniej metody klasy Record Store. Najpierw sprawdzamy czy zbiór rekordów nie jest przypadkiem już zamknięty. Wtedy wywołujemy metodę closeRecordStore(). W przypadku wystąpienia błędu (metoda ta może zwrócić wyjątek) upewniamy się, że pamięć zaalokowana na rs zostanie zwolniona przez przypisanie rs wartości null w bloku finally. W ten sposób nawet jeżeli próba zamknięcia schowka się nie powiedzie, odśmiecacz powinien zwolnić pamięć zaalokowaną na rs // zapisz wiadomość o poziomie FINE // zwróć poziom logowania // Zapisz wiadomość o poziomie INFO // czy dany poziom błedu jest logowany - innymi słowy - czy komunikat public boolean isLoggable( Level test ) // w poniższym bloku odwołujemy się do getLevel() kilka razy. // zapisz daną wiadomość msg o poziomie logowania level // jak wyżej, ale dodatkowe informacje o zgłoszonym wyjątku // zapisujemy tak wiele informacji jak się da // jeżeli mamy logować do stdout if( !logToRS ) return; // przygotowujemy strumienie danych //zapisujemy wszystkie dane do strumienia...prowadzącego do tablicy
bajtowej
// i zapisujemy w całości do schowka - w ten sposób zamiast zapisywać synchronized( this ) try } // Ustalenie poziomu logowania //czy logować na stdOut public void setLogToStdOut( boolean log ) // czy logować do Record Store
public void severe( String msg ) // zapisz wiadomość o poziomie WARNING public void warning( String msg )
Tomasz Rybicki literatura i dodatkowe informacje emulatory telefonów obfuscator |