When we are writing Android code, our focus is mainly on the UI, event handling, background task and backend integration. We hardly think about the performance of the app and usually take it for granted.
Here we are going to look into some important aspects, which heavily impact the performance of our app, and also understand the steps, which can be taken by the developers to improve the performance.
Normally, the system will try to redraw the activity every 16ms or so. How does this impact us?
- The phone’s hardware determines the speed at which the screen can update itself. Most devices refresh at about 60Hz i.e 1000ms/60Hz~16.66 ms
- Thus the system has just about 16ms to execute all the drawing logic.If the 16ms interval is missed, it leads to dropped frames.This hinders smoothness in the animations. Multiple such instances lead to jank and a laggy experience.
- Thus we must keep in mind that fancy animations and transitions may not work very well on all Android devices.
The Android rendering pipeline is broken into 2 subsystems: GPU and CPU.
The process of converting XMLs to screen is called rasterization.
- Those who have made a custom view, might be aware about the steps of rendering a view (as depicted below)
- Rasterization is a very time consuming process and the GPU is built to make this process faster. Thus we should try to minimize these steps as much as possible.
- If we keep modifying the objects e.g. changing background color, text, size etc, the above process will re-execute every time.
- Rendering process gets more and more time consuming as the UI gets more and more complex.
- On the GPU side, the most important problem is “overdraw” i.e. wasting GPU cycles to draw and color pixels, which eventually get hidden.
- Overdraw is a term used to describe how many times a pixel on a screen is redrawn in a single frame. E.g.in a bunch of stacked UI cards, only the top most card is visible, however all cards are drawn.
- Developers can keep the following in mind, while writing the UI code.
i)Each time we are drawing something, which is not part of the final scene, we are wasting GPU cycles.
ii) Avoid stacked or layered UI.
iii) To see overdraw on device : Go to Developer mode and turn on “Show GPU Overdraw” option.Android uses color scheme to show level of overdraw. Look for areas of red to reduce overdraw. Eliminate unneeded background colors. And look for areas which will be hidden.
CPU’s role in rendering:
- In order to draw something on the screen, the CPU needs to convert high level XMLs into objects which the GPU can understand. This is done with the help of a display list.
- If we need to render an object again e.g. If the position of a UI element changes, we need to recreate and re-execute the display list since the view gets invalidated.Also, If other views are re-positioned because of a change in one view, the display list of every layout is recreated and re-executed.
- When sizing of view changes, the onMeasure() method is called again and the entire view hierarchy is re-examined.
- If the position of view is changed, then requestLayout() method is called again.
- Thus, if the view hierarchy is very complex, “measure” and “layout” parts of the rendering pipeline can cause performance problems.
- Hierarchy viewer helps to understand the entire layout and eliminate rote views and can also help to flatten out the view hierarchy.
- As developers, we need to keep in mind that nested layouts impact the performance and responsiveness of the UI. e.g. use constraint layout and relative layout instead of nested linear layout.
In order to understand memory optimization, we need to understand the basics of Garbage Collection.
- Even though Java comes with a Garbage Collector and memory management is not the developer’s primary concern, there can still be performance problems due to memory leaks.
- Garbage collector, simply put, works in 2 steps: Mark and Sweep i.e.find objects that can’t be accessed and reclaim the resources from them. But it is very very difficult to figure out which objects are not accessed ,when to run the garbage collector, and avoiding fragmented memory ,thus making it very complicated.
- Memory heap in Android runtime is segmented into different spaces based on the type of allocation and how best the system can organize the objects for future GC events. As a new object is allocated, these characteristics are taken into account and the heap space with the best fit is selected.
- Each space has a set size.As objects are allocated, the system keeps track of combined sizes. As the space grows, the system executes a GC to free up space.
- GC behavior depends on Android runtime. In Dalvik, GC operations may stop the current executing process until it is completed. This can slow down the process if GC takes more time or GC is invoked too frequently. E.g. If GC takes longer, it can make draw operation miss the 16ms frame, which in turn causes jank.
- The new ART GC or generational GC is more optimized and runs in parallel with the current running process..
- The more time is consumed during GC, less time is allotted for other tasks like rendering or computing.
- Objects which are not used and still not freed by GC cause memory leaks.
Memory monitor can show how the application uses memory over time.
What are memory leaks?
- Each heap segment has a memory limit. When the limit is almost exhausted, a GC is triggered. Very frequent GC events can hinder performance.
- Memory leaks lead to very frequent GCs.Leaked objects are those which are no longer needed but the garbage collector fails to recognize that they are not accessed anymore. Thus the available space in the heap gets smaller and smaller.This leads to a scenario when more GCs will be executed more often to free up space.
- Memory leaks are slow and insidious. It might take us days to realize that we have a memory leak.
Heap Viewer can help us see how much memory is being used by our application. We can know “what’s” on the heap.
Common memory leak scenarios:
- Use of static view/context : Static views and context objects will be held in memory till the application is killed and the memory occupied by these will not be garbage collected. Avoid using context as a static variable or in a companion object in Kotlin.
- Forgetting to unregistering listeners: If we do not unregister the listeners, the memory occupied will not be reclaimed by the garbage collector, even though it is not in use.This problem is compounded when the activity is destroyed and new one is created e.g. on device rotation. Every time a new listener is created by the view, which is never released. Thus no listener is ever reclaimed by GC and this created a memory leak.
- Non-static nested class: If the nested class is not static, it will implicitly hold the reference to the outer class, and thus the outer class will be alive till the application is alive.
- Passing context to third party libraries: Always pass getApplicationContext() to initialise third party libraries, otherwise activity context will be used in a static context by the library and the activity will not be garbage collected till application is killed.
- Bitmaps: Bitmaps can easily exhaust applications memory. Thus we must reduce, reuse and recycle bitmaps as much as possible. We can also use third party libraries like Glide which help in optimizing usage of bitmaps.
Battery and network
Though it’s not very obvious, some of our code may end up draining more battery.
- There is a huge spike in battery drain every time we turn the screen on; turning on LEDs, painting the screen, CPU,GPU operations etc require a lot of power.
- There is a huge battery drain when the application wakes up the device.e.g. Wake lock, alarm manager or job scheduler. Keeping the device awake to do little or no work is a waste of battery life.
- To reduce battery drain from wake lock, defer battery intensive operations to time when the phone is charging, or connected to Wifi. Job scheduler API is the right choice to schedule or batch future operations.
- Networking is the biggest culprit for battery drain.There is a radio chip, which communicates with the cell tower and communicates data with them in high volumes.If possible, perform background network operations when connected to WIFI, so that cellular radio is not used. However, writing the code to batch, cache and defer networking requests is a really tricky task.Job Scheduler APIs can help here too.
Network profiler can be used to visualize this:
If we keep these aspects in mind while developing the app, we can avoid lot of performance problems in our application from the very beginning.
This article was originally published on proandroiddev.com on July 31, 2022