czwartek, grudnia 29, 2011

Dlaczego GUI na Androidzie nie działa płynnie?

Jeśli ktoś ma tablet z Androidem i do przeglądania Internetu używa Firefoksa to zapewne zauważył, że działanie aplikacji wcale nie jest płynne (także w kontekście obsługi zdarzeń generowanych przez ruchy palca po ekranie). Android operuje na buforze ramki karty graficznej, wątek do rysowania jest jeden. Podejście takie samo jak w Javie. Napiszmy zatem w Swingu program obrazujący problem: na panelu rysujemy pod kątem ostrym naprzemienne linie czerwone i czarne z włączonym antyalisingiem.


Okienko aplikacji pojawia się bardzo wolno, skalowanie okna stwarza problemy wydajnościowe, pod względem używalności GUI jest słabo. Jak to można naprawić? Trzeba wziąć pod uwagę, że jeżeli czas obsługi zdarzenia/rysowania jest znaczny w porównaniu do czasu generowania zdarzenia (ruchu palcem po ekranie), to może nam się nagromadzić kolejka zdarzeń do obsłużenia. W poprawnej obsłudze stanu aplikacji, część zdarzeń powinna być odrzucona, a obsłużone tylko ostatnie. Przy takim założeniu oprócz wątku obsługi zdarzeń, którego działanie powinno być natychmiastowe, potrzebujemy dodatkowego wątku, który będzie wykonywał pracę graficzną. Pojedynczy wątek podsystemu graficznego powinien być używany tylko do blokowego transferu gotowego obrazka do bufora ramki (BitBlt).


Mniej więcej takie założenia ma Apple Grand Central Dispatch i dzięki temu GUI na iOS śmiga*. Na Androidzie też się da, ale musi zadbać o to programista. Na urządzeniach przenośnych trzeba wypracowywać kompromis pomiędzy zużyciem pamięci (offscreen pixmaps) a responsywnością, jednak Firefox powinien brać pod uwagę, że tablet to trochę większe urządzenie i da się na nim osiągnąć lepsze user experience. Ciekawe jest podejście Google-a do przeglądarki: strona wyświetlana jest tylko, kiedy cała jest gotowa, a żeby zająć czymś użytkownika u góry przebiega ładna ale za wąska linia postępu. Bufor pozaekranowy oczywiście istnieje.


Ostatni obrazek pokazuje artefakty wynikające z odmalowywania na ekranie obrazka w trakcie rysowania - ale oszczędzamy na jednym buforze pozaekranowym.

O czym powinien jeszcze pamiętać programista na platformie ARM? O minimalizowaniu ilości rozgałęzień kodu. Spekulatywne wykonywanie zaczyna pojawiać się w tych procesorach, ale nie jest na tym samym wysokim poziomie jaki prezentuje Intel x86. Z drugiej strony wykonywanie kodu, które pójdzie w niebyt, niepotrzebnie marnuje energię.

* - nie zmienia to jednak faktu, że posiadanie iPhone-a przez szwagra jest mało męskie.

Ciekawostka: Java 1.8.0-ea-b18 po chwilowej zmianie rozmiaru okna na [0,0] wisi na:

----------- Thread[Thread-2,6,main]
sun.java2d.d3d.D3DRenderQueue.flushBuffer(Native Method)
sun.java2d.d3d.D3DRenderQueue.flushBuffer(D3DRenderQueue.java:152)
sun.java2d.d3d.D3DRenderQueue.flushAndInvokeNow(D3DRenderQueue.java:142)
sun.java2d.d3d.D3DSurfaceData$D3DDataBufferNative.getElem(D3DSurfaceData.java:448)
sun.awt.image.DataBufferNative.getElem(DataBufferNative.java:75)
java.awt.image.DataBuffer.getElem(DataBuffer.java:327)
java.awt.image.SinglePixelPackedSampleModel.getDataElements(SinglePixelPackedSampleModel.java:409)
java.awt.image.Raster.getDataElements(Raster.java:1469)
sun.java2d.loops.OpaqueCopyAnyToArgb.Blit(CustomComponent.java:144)
sun.java2d.loops.GraphicsPrimitive.convertFrom(GraphicsPrimitive.java:560)
sun.java2d.loops.GraphicsPrimitive.convertFrom(GraphicsPrimitive.java:541)
sun.java2d.loops.MaskBlit$General.MaskBlit(MaskBlit.java:189)
sun.java2d.loops.Blit$GeneralMaskBlit.Blit(Blit.java:204)
sun.java2d.pipe.DrawImage.blitSurfaceData(DrawImage.java:959)
sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:578)
sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:71)
sun.java2d.pipe.DrawImage.transformImage(DrawImage.java:1111)
sun.java2d.pipe.ValidatePipe.transformImage(ValidatePipe.java:238)
sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3214)

Z dokumentacji Androida:

Tips and Tricks
Switching to hardware accelerated 2D graphics can instantly increase performance, but you should still design your application to use the GPU effectively by following these recommendations:
Reduce the number of views in your application
The more views the system has to draw, the slower it will be. This applies to the software rendering pipeline as well. Reducing views is one of the easiest ways to optimize your UI.
Avoid overdraw
Do not draw too many layers on top of each other. Remove any views that are completely obscured by other opaque views on top of it. If you need to draw several layers blended on top of each other, consider merging them into a single layer. A good rule of thumb with current hardware is to not draw more than 2.5 times the number of pixels on screen per frame (transparent pixels in a bitmap count!).
Don't create render objects in draw methods
A common mistake is to create a new Paint or a new Path every time a rendering method is invoked. This forces the garbage collector to run more often and also bypasses caches and optimizations in the hardware pipeline.
Don't modify shapes too often
Complex shapes, paths, and circles for instance, are rendered using texture masks. Every time you create or modify a path, the hardware pipeline creates a new mask, which can be expensive.
Don't modify bitmaps too often
Every time you change the content of a bitmap, it is uploaded again as a GPU texture the next time you draw it.
Use alpha with care
When you make a view translucent using setAlpha()AlphaAnimation, or ObjectAnimator, it is rendered in an off-screen buffer which doubles the required fill-rate. When applying alpha on very large views, consider setting the view's layer type to LAYER_TYPE_HARDWARE.
Słabo, prawda?

0 komentarze: