Treść tego wpisu mógłbym zawrzeć w jednej sentencji: “nie używaj DrawString” i zasadniczo mógłbym zakończyć, ale postaram się udowodnić dlaczego.
Zacznijmy od testów:
e.Graphics.MeasureString("Testowy string", base.Font);
e.Graphics.DrawString("Testowy string", base.Font, new SolidBrush(Color.Black), 0, 0);
e.Graphics.DrawImage(img, 0, 0);
Trzy metody; pierwsza mierzy nasz napis, druga rysuje a trzecia jest dodatkowo dla porównania, ale do niej zaraz wrócimy.
Uzyskane wyniki w trzech próbach przedstawiają się tak (ticks | miliseconds):
MS – MeasureString
DS – DrawString
DI – DrawImage
MS: 647855 | 37
DS: 862750 | 34
DI: 26575 | 1
MS: 2483094 | 101
DS: 705654 | 28
DI: 310075 | 12
MS: 650258 | 36
DS: 880659 | 34
DI: 43816 | 1
Wyniki są rozbieżne w różnych próbach ale nie to jest ważne, chodzi o ogólne proporcje między nimi. Na pierwszy rzut oka widać od razu, że najwolniejsza metoda odpowiada za pomiar napisów, druga co do powolności jest metoda odpowiadająca za rysowanie.
Wiemy już co jest wolne ale jak wyeliminować te metody w naszych projektach na Windows Mobile. Odpowiedź jest dziecinnie prosta, cache i jeszcze raz cache. Najbardziej trywialny sposób to jest dekorator który będzie zawierał dodatkowe grafiki napisów, np coś takiego:
internal class MessageDecorator : Message, ICloneable
{
private Image _bodyImage;
internal Image BodyImage
{
get { return _bodyImage; }
set { _bodyImage = value; }
}
}
Jeżeli BodyImage jest nullem przy pomocy prostego helpera może stworzyć sobie cache:
internal static Bitmap Draw(this String s, Brush background, Font font, Brush brush, RectangleF rectangle)
{
Bitmap buffer = new Bitmap((int)rectangle.Width, (int)rectangle.Height);
using (Graphics g = Graphics.FromImage(buffer))
{
g.FillRectangle(background, 0, 0, buffer.Width, buffer.Height);
g.DrawString(s, font, brush, new RectangleF(0, 0, buffer.Width, buffer.Height));
}
return buffer;
}
W przypadku gdy nie jest nullem grafikę body wrysowujemy w miejsce gdzie powinna się znajdować przy pomocy DrawImage(Image, x, y). Ważne jest to aby nie używać innych metod abyśmy czasem nie skalowali naszych napisów. Dlatego cache tworzymy w skali 1:1.
Na koniec pamiętajmy jeszcze o skasowaniu cachy gdy na telefonie zmieni się orientacja ekranu.
protected override void OnResize(EventArgs e)
{
if (_messages != null)
{
foreach (MessageDecorator message in _messages)
{
message.BodyImage = null;
}
Refresh();
}
base.OnResize(e);
}
Kolejne odpalenie OnPaint spowoduje przerysowanie napisów w nowych rozmiarach. Oczywiście kasowanie ma tylko sens, jeżeli napisy są dokładnie wpasowywane w wielkości ekranu. W przypadku pojedynczych, jednowierszowych napisów nie ma to sensu.







Uno
/ 2009-06-29Chyba przesadziles z ta wydajnoscia. Przede wszystkim nie napisales jak mierzyles tzn ile razy narysowales ten string i na jakim sprzecie. Ja nie wyobrazam sobie tworzenie cacha dla kazdego stringu (marnujemy RAM) zwlaszcza jak masz rysowanie warunkowe czyli np zmieniasz kolor stringa w zaleznosci od jakiegos parametru no i uzywanie cacha jest bardzo niewygodne i wprowadza niepotrzebne komplikacje w kodzie.
Jakub Florczyk
/ 2009-06-29@Uno
Faktycznie nie napisałem jak mierzyłem i na jakim sprzęcie, bo tak jak napisałem we wpisie chodzi mi o proporcję a nie konkretne wyniki.
Masz rację, że w przypadku “bardzo” dynamicznego tekstu, który często się zmienia cache jakikolwiek nie ma sensu. To jest przykład cache dla tekstów które się rzadko zmieniają i wbrew pozorom zużycie pamięci nie skacze dramatycznie jeżeli użyje się go “z głową”.
Grzegorz Aksamit
/ 2009-08-03@Jakub Florczyk
Możesz rozwinąć co masz na myśli mówiąc “z głową”?
Obiekty typu Bitmap w .NET CF są strasznie pamięciożerne – nawet po załadowaniu z pliku JPG – w pamięci i tak trzymana jest bitmapa. Zakładając rozmiar ekranu taki jak np w HTC Touch HD (480×800), na prosty nagłówek ekranu powiedzmy 400×50 marnujemy od razu 80kB (dobrze licze?) A to tylko jedna z wielu grafik w aplikacji.
Usuwanie tego cache i tworzenie go na nowo też nie wydaje się do końca dobrym pomysłem.
Garbage Collector w .NET CF jest dość prymitywny – nie śledzi dziur w stercie które powstają w wyniku niszczenia obiektów. Jeśli tworzymy obiekty A B i C sterta wygląda w przybliżeniu tak: |A|B|C| Jeśli teraz zniszczymy obiekt B i utworzymy D to sterta wygląda tak: |A| |C|D| itd. Jedynie raz na jakiś czas GC wykonuje tzw. heap compaction, gdzie przeczesuje całą stertę w poszukiwaniu dziur a następnie przesuwa wszystkie obiekty po stercie tak, żeby nie było dziur – a to jak wiadomo strasznie czasochłonna i zamulająca operacja. Chyba jeszcze gorsza niż rysowanie tych tekstów DrawString()-iem. Ale do tego trzeba by wykonać więcej testów.
Jakub Florczyk
/ 2009-08-07@Grzegorz Aksamit
“Z głową” mam na myśli jak najmniejsze obiekty i tylko te rzadko zmieniane. Wiem że to nie argument ale aplikacje .NET i tak są pamięciożerne, więc nawet +0.5MB nie robi im różnicy jeśli się przekłada na użyteczność.
Tak czy inaczej ja tylko pokazuje jedną z metod, która wg. mojej opinii jest użyteczna; jeśli inni tak nie uważają to nie muszą jej przecież używać
W Pocket Blip użyta jest powyższa metoda i mimo wzrostu zużycia pamięci +20% zadowolenie użytkowników wzrosło +100%
Na koniec polecam wszystkim przeciwnikom zrobić mały test: wyrysować pokolorowany tekst (tak jak na statusie blipa gdzie pokolorowane są tagi, linki, użytkownicy) przy pomocy DrawString a potem zrobić płynnego scrolla po ekranie.