blog.echolocker.com/performance

One of the hardest parts of building for Android is making your app work well on all phones. While device fragmentation often brings forth concerns on design, the bigger struggle will be behind the scenes in managing memory, rendering smooth graphics, and maintaining battery life.

iOS hardware is generally fast enough to run animations smoothly and avoid out of memory errors. The behavior of Android phones, on the other hand, is more unpredictable. In planning an Android app, a good rule of thumb is to 1.5x the days it took to build your iOS app to create the same polish and quality.

This post contains the tips we learned on optimizing memory, graphics, and battery.

Memory drives retention and ratings

If you have a steep uninstall rate on day one, excessive memory use may be the culprit. Android runs on lower-end devices and has a greater coverage of international users. If your app is a memory hog, these users will not only have a poor experience but will also uninstall your app to free up memory for other services. 

Understanding how users perceive memory

What do people mean when they say your app uses too much memory? They are likely referring to the settings screen below.

Notice that the memory number is not the private memory in use, but rather the total memory paged. Even if your app has no memory leaks, it may have a generous heap allocation for your SDKs. 

The Android Device Monitor tool in Android Studio only shows private Dalvik heap allocation, and one common mistake is to overlook the shared memory. Read the section on allocating and reclaiming memory and this post for more details.

Reuse and release to manage memory

Google has good resources on memory management (read the section on “how your app should manage memory”) and managing bitmaps. While most lockscreens use +50MB of memory, Echo only uses ~30MB. This post covers the strategies we used to reduce our memory footprint.

Cache selectively: Caches help reduce memory by avoiding repeat allocation of bitmaps. However, some forms of caching actually increase memory usage and its important to figure out which ones help your app. For Echo, we found the scrolling cache on a listview with variable cell height dramatically increased memory allocation (+40MB difference). 

Reuse objects: The best way to reduce memory allocation is to avoid creating new objects. Dalvik uses mark and sweep with no compaction for garbage collection. Thus memory fragmentation compounds allocation issues. By reusing objects, you reduce peak memory allocation and minimize fragmentation. In particular, bitmaps are common culprits and you should reuse them when possible.

Release shared libraries: A simple way to reduce memory is to remove unused SDKs or only launch them at the instance they are needed. While Android shows the split between your private and shared memory, most users ignore the difference and only look at the total memory consumed. Many SDKs consume a surprisingly amount of RAM. Google Analytics and Google Billing use over 20MB. Even if you use an SDK on iOS, re-evaluate the memory usage on Android because there are quality differences between platforms.

Use Eclipse Memory Analyzer (MAT) early and often: This is a great tool to trace memory leaks and analyze allocations. Test your memory usage on every update because it’s easier to track down a leak in a recent change. The hprof conversion tool is also built into Android Studio.

Use ProGuard to compact your code: Your app’s code is memory mapped by Android. More lines of code means a bigger memory footprint that is reflected in the ‘.so’, ‘.ttf’, ‘.apk’ and ‘.dex’ rows in ADB’s meminfo. Proguard reduces the lines of your code, but read up on of how ProGuard works because it can break reflection.

Test on phones with large screens: Pixels are denser on screens with higher resolutions. On devices with large screens, like the Samsung Tab, the heap allocation can double or triple to account for bitmaps. Test your app on large screens to spot memory issues.  

Hack: attribute the memory elsewhere: As a last resort, you can trick Android into showing a lower memory profile for your app. This approach is not recommended because it doesn’t make your app smoother or faster. To hack a lower memory profile, you can launch libraries that are nonessential to the main thread in separate processes. The heap allocated by this process will not be attributed to your app, and users will see a lower memory profile. This is a band-aid of last resort that should be used sparingly.

Use simple layouts and caching for smooth graphics

Even with hardware acceleration, rendering lag can be noticeable. Here are some tips to optimize graphic performance:

Use opaque pixel type: There are three pixel types: opaque, transparent and translucent. Of these three, opaque (no alpha) is the fastest to render and translucent (has alpha) is the most expensive. With opaque pixels, Android does not have to render background views so its easier to optimize for performance. Avoid animations with alpha because they are rendered in an off-screen buffer that doubles the required fill rate.

Avoid nested layouts: Relative layouts are your best bet when balancing design flexibility and rendering cost. Although relative layouts include an additional pass for measuring, achieving the same design would otherwise require an expensive group of nested linear layouts. Android Studio comes with a UI hierarchy viewer that can be helpful in spotting duplicates in your layout structure. This blog post has good tips on avoiding viewgroup wrappers.

Cache large images in a separate view: Setting a large photo to the background is a popular design (ex: Yahoo Weather). However, these photos need to be cached to avoid animation lag. Instead of setting the photo directly as the background, place that image into separate view in order to can cache the image (eg. use hardware layer type).

Avoid animations: While animations run smoothly on your Nexus 5, users on low end phones may see a significant lag. Due to the unpredictability of animations, its best to avoid them. We ended up scrapping most of the animations from V1 of Echo because it took too much time to maintain and optimize them across all devices. The result was a faster app with fewer lags.

Use system animations: If you must use animations, use system animations. Android comes with many animation options. Some of these animations are not documented, but you find them by exploring the source code. At this link you can find hidden animations that are not exposed through the official SDK. These animations are already optimized so you don’t have to reinvent the wheel.

Sensors and services can drain battery

Battery drain is a common complaint on Android, and Google has some good tips on managing battery. Although Android phones have larger batteries, this bonus is offset by the increased power consumption. Android needs 2-3 times more RAM than iOS for background processes and garbage collection. Servicing more RAM requires more power. Here are a few other tips to reduce your battery impact:

Kill background services when not needed: Don’t be greedy and kill unused processes. Background services can be a huge drain on battery when poorly managed. Few apps, including battery saving apps, manage their service well. Ironically, running the “battery saving” services can consume more battery than they save. Design your app without background processes if possible.

Optimizing your scheduling service: AlarmManager and ScheduledExecutorService are two scheduling classes. ScheduledExecutorService runs in your application process. If the app dies, none of the scheduled tasks will run. AlarmManager, on the other hand, is a system service that runs all the time. If your app scheduled a task and was killed, AlarmManager may restart the app to run the task. Default to ScheduledExecutorService since it only runs when your app is open and is less likely to drain battery. If you need to use AlarmManager, setInexactRepeating() to minimize power. With inexact wakes, Android synchronizes alarms from multiple apps and fires them at the same time. This reduces the total number of times the system must wake the device. Post 4.4, all AlarmManager alarms are inexact.

Be careful with sensors: There is no standard on how frequently sensors can trigger/poll. For example, the same BatteryManager class can be triggered anywhere from every 100ms to 1min on different phones. The varied polling results in an inconsistent battery usage. It’s better to poll manually to force sensors to behave consistently. Only turn on power consuming sensor, like proximity and location, when needed.

Creative ways to save power: For Echo, a large chunk of our battery usage is for screen display. To avoid this, Echo only lights up the user’s screen when it’s a priority message. We also used a combination of signals (time, proximity sensor) to avoid unnecessary wake ups and decreases the impact on the battery. 

As background, we were part of YC and build Echo, a smart lockscreen for your phone. We’d love to hear from you. Reach out at founders@doublelabs.com


Comments (0)

Sign in to post comments.