In January I was playing with one of the apps I’ve been working on and I’ve come across something interesting — there was a noticeable stuttering of one of the screen transition animations. That was somewhat unexpected**, since I haven’t touched this part of code in ages, and still here it was, right before my very own eyes.
Part 1 — First ideas (StrictMode, Logcat)
“Ok”, I thought to myself, “Rookie Mistake 101 — Doing IO on the Main Thread”. The second screen was mostly occupied by a
RecyclerView with the content taken from the SQLite database, so it was natural to think that the app was doing too much work on the main thread (this screen wasn’t backed by any paging solutions, such as Paging library). I knew I was using StrictMode on debug builds, but it never hurts to double-check, right?
So I opened my Application class and checked whether StrictMode is enabled or not. It’s there, there were no changes to this file for a pretty long time (
git blame is my best friend in situations like this one). I opened Logcat, set the filter by “StrictMode” and performed this transition several times. No luck — no warnings, nothing.
Hmm, is it possible that this is some random glitch with StrictMode itself? I quickly changed the implementation of one of my caching stores used on these screens so that it was using the main thread instead of the IO thread and checked Logcat again.
D/StrictMode: StrictMode policy violation: android.os.strictmode.DiskReadViolation
There you’re! Ok, StrictMode is enabled, there is no IO on the main thread — what else could it be and how could I find it?
Part 2 — Developer Settings (GPU Overdraw, Profile HWUI rendering)
Every Android developer should study “Developer Settings” area in-depth since there’re many useful things in there, from the ability to tweak the animation speed to location mock apps. The things I was mostly interested in, though, were in “Hardware Accelerated Rendering” and “Monitoring” sections. Let’s start with GPU Overdraw.
In a nutshell, overdraw is when you draw the content several times over the same area on your screen. As an example, you might have a background attribute applied to the root view which spans across the whole screen, then you might have an
ImageView with its background, and, finally, this
ImageView might have an image defined as its
src attribute. In this case, you’ll end up drawing three times — first the root view’s background is drawn, then you draw the
ImageView’s background on top of it, and, finally, the source image will be drawn. These kinds of things are not for free and they can significantly increase the amount of time required to perform the frame’s rendering***.
Once you enable GPU Overdraw, Android starts marking the overdraw area with special colors, similar to how it’s done on that screenshot:
If some area is drawn with its normal color, then there is no overdraw at all. Red color, though, means that pixels were overdrawn 4 times or more.
So, I enabled this option and looked at the second screen, the one which was shown at the end of transition animation. There was some overdraw, indeed, but there was nothing which was overdrawn more than 2 times. It couldn’t explain the fact that the animation was lagging on the Pixel 2 XL device, which is still fairly good, even by 2019 standards, so the issue had to be somewhere else.
The next thing I decided to check out is called “Profile HWUI rendering”. What it does is it shows how much time does it take to render each frame compared to a benchmark of 16ms per second. You can choose one of two options, “On screen as bars” or “In adb shell dumpsys glxinfo”.
I decided to use the first one since I wanted to get a quick feeling of what exactly goes wrong. In this mode, Android draws vertical bars for each frame and each bar is represented by different colors, related to each stage of frame rendering. Orange component corresponds to the “Swap buffers” stage, light green corresponds to the measure/layout stage, etc. I refer you to the official documentation for the details, but I’ll still drop a screenshot here in case you want to proceed with the story:
I enabled this mode and looked at the results. Here it was — a huge spike in green areas, so I immediately learned two things:
- Most likely I’m color blind since I couldn’t figure out whether it was measure/layout or animation phase (or maybe, just maybe it was a poor selection of colors for this tool, but I don’t want to be a judge here).
- There had to be something going on with animations and measure/layout phase during the transition animation since these bars were never as high when I was simply scrolling the
RecyclerView’s content. As far as I remember none of these frames dared to violate the 16ms per second contract.
The thing that I tried to do next was to simplify the
RecyclerView item’s UI to the bare minimum — just a single
TextView, nothing more. I wanted to see if having a simpler layout makes a difference at all. In the end, there was some improvement, but these lags were still happening from time to time.
This was probably the right time to uncover some heavy artillery tools (aka Systrace), but, since I’m a lazy person I decided to do one small round of googling, just to see if I find something useful before I go too deep down the rabbit hole.
Part 3 — Chet Haase (nothing extra in parentheses, sorry)
I found a four-year-old article written by Chet Haase related to just what I was looking for (note: Chet Haase is similar to Rome to me, in that any time I search for something related to UI performance, eventually I end up reading his articles or watching his videos. “All roads lead to Chet”, as someone would say).
Here goes the relevant piece of the aforementioned article:
As mentioned in the UI Thread section in the Context chapter, expensive operations which happen on the UI thread can cause hiccups in the rendering process. This, in turn, causes problems for animations, which are dependent on this rendering process for every frame. This means that it is even more important than usual to avoid expensive operations on the UI thread while there are active animations… To avoid this situation when layout needs to occur, either run the layout operations before animations start or delay them until the animations are complete.
Ok, nice. Actually, it makes sense — trying to populate the
RecyclerView with the data involves going through the measure/layout phase multiple times, and, since my cache layer was popping up the data to be shown right when the transition between the first and the second screens’ views was still happening, this had to contribute a lot to the junk I saw on my device.
What if I do a quick dirty fix by delaying the operations which load the data from the local database and offload it to
RecyclerView until the screen transition is done? This can be done by introducing the hard-coded delay via RxJava’s delay operator.
Yeah, much better, no stuttering now. All praise Chet Haase!
The proper solution
The proper solution was just a tiny bit harder than hard-coding some arbitrary value. Since I was using the library called Conductor, all I had to do was to subscribe to the source of entities at a proper moment in
Controller’s lifecycle, specifically the method called
onChangeEnded which is called when
Controller completes the process of being swapped in or out of the host view. Transition animation happens first, then goes the subscription and measure/layout related to it.
Ok, that’s it for today. The main lesson, as always — dedicate the time to learn the Android framework’s intricacies as it’ll surely pay off in the future.
Thanks for reading!
* The word “jank” has several meanings in Android terminology but in this article, it’s used to describe the behavior of an app that can’t consistently deliver the new frames to be rendered within 16ms, resulting in dropped or delayed frames.
** What’s interesting, is that this issue was affecting only the Pixel 2 XL device which I had when I first noticed these lags. When I switched back to my old Nexus 5 device which is was I’ve been using when I started to work on that project, I couldn’t see any of such problems. Probably Systrace could’ve helped me with figuring out exactly why one device was affected and another one, having much better specs, wasn’t.
*** When I was preparing this article, I decided to re-read the documentation on overdraw and I found that it might not be as bad as it used to be back in 2013–2014 when I was working on my first Android apps:
Overdraw is no longer as significant a problem as it was when discussed in Google I/O performance sessions, and performance pattern videos. This is because low-end devices have continued to grow in GPU performance, while their displays have plateaued at relatively low resolutions. Unless optimizing for a known low-performance GPU device, it’s recommended to focus on optimizing UI thread work instead to ensure smooth app performance. In addition to this, OS optimizations avoid overdraw within your app in many cases (for example, a Fragment background overdrawing the window background).
Still, it makes sense to keep a close eye on it, since even a small leak can sink a great ship.