Why Android Applications Skip Frames and How to Fix This Issue

This article describes how Android applications draw their user interface on the screen, what does skipping frames mean for your users and how to solve this problem.

How Android Applications Draw on the Screen

Fundamentally, you can think of device’s screen as a two-dimensional matrix of small light sources called pixels. Each pixel can be set to emit a light of a specific color. Consequently, by controlling the colors of individual pixels, applications can “draw” on the screen.

Since a screen is just a matrix of pixels, when an app needs to show anything on it, app’s state should be translated into two-dimensional array of color values. This process is largely handled by Android itself, so we, developers, get to work with higher-level constructs like TextView, ImageView, etc. Eventually, after all the tasks associated with screen refresh are executed, the screen will show a specific image. This image is called a “frame”.

Frame Rate

Unfortunately, the aforementioned translation of app’s state into an array of color values is a time-consuming operation. Furthermore, after this array is computed, it should then be “applied to the screen”, which takes time as well. Therefore, it would be unpractical to show a new frame each time your application changes its state.

Instead of constant refreshes, Android schedules periodic refreshes of the screen. This way, new frames are show at predefined moments in time, and they reflect all state changes that took place up until that point. The frequency of these periodic refreshes is called a “frame rate” and it is expressed in “frames per second” (FPS).

Historically, most Android devices supported a frame rate of up to 60 FPS. Today, we have plenty of devices that can go as high as 120 FPS. However, for the purpose of our discussion here, I’ll stick with the “standard” 60 FPS.

Skipped Frames

What does it mean for a device to operate at 60 FPS? Well, it means that every ~16 milliseconds it needs to refresh the screen and show a new frame. Consequently, all applications that want to draw on the screen, should report their state to the system during this interval of time.

However, if the app does too much work on its User Interface (UI) thread, it might fail to complete all the required preparations and report its state within the aforementioned time window. When this happens, the system will simply skip screen update and just show the previous frame. Then, your app will get a chance to update the screen only after additional 16 ms. That frame, which wasn’t shown because it hadn’t been ready on time, is called a “skipped frame”.

Skipped frames are a major issue because your application looks bad when it skips frames. Depending on the number and the frequency of skipped frames, the app might feel anywhere from “not smooth” to totally stuck. This issue affects end users directly, so that’s not something you can ignore.

Skipped Frames Warnings

Luckily for us, we get automatic warnings about skipped frames when we develop our applications. They come in the form of messages like this one in logcat:

I/Choreographer: Skipped <number> frames! The application may be doing too much work on its main thread.

Choreographer class is part of Android’s UI framework, and “UI thread” and “main thread” are synonyms for pretty much all practical purposes. So, Choreographer not only warns you about skipped frames, but it even suggests where the problem might come from.

By testing on low end devices and watching for these messages when you develop your app, you can ensure that it will work smoothly on an average Android device out there. If you do automated UI testing, then you can further add these checks to your regression suite.

Thread Starvation

Despite the suggestion in Choreographer’s messages, even if your app utilizes UI thread properly, it can still skip frames.

For example, imagine that there are many live threads (in addition to UI thread) which compete for CPU time. The system attempts to be fair and gives each one of them an opportunity to utilize CPU, but, as a result, UI thread doesn’t get enough CPU time to complete frame preparation in 16 ms. Consequently, some frames are skipped. This situation is called “UI thread starvation”.

Therefore, when you debug skipped frames, keep in mind that the root cause can have nothing to do with the login that executes on the UI thread itself.

Skipped Frames Warning Threshold

If you’ll put UI thread to sleep for different time periods, you might notice one interesting aspect of Choreographer’s warnings. Turns out that it doesn’t issue warnings for less than 30 skipped frames (~500 ms without screen updates).

Let me repeat that again: if your app skips less than 30 frames at a time, Choreographer won’t notify you about skipped frames at all! This is a considerable period of time and users will undoubtedly notice if your app becomes frozen for that long. Therefore, this threshold of 30 skipped frames before you see Choreographer’s warnings is too high.

Changing Skipped Frames Warning Threshold

To understand where the aforementioned threshold comes from, I decided to review Choreographer’s source code. It didn’t take much time to find this statement:

private static final int SKIPPED_FRAME_WARNING_LIMIT = SystemProperties.getInt("debug.choreographer.skipwarning", 30);

Looks like 30 is the default limit, but it can be overridden with debug.choreographer.skipwarning system property. Luckily, this property starts with “debug”, which means that we can change it even on non-rooted devices.

So, I issued the following shell command:

adb shell setprop debug.choreographer.skipwarning 5

and made sure that it had an effect:

$ adb shell getprop debug.choreographer.skipwarning
5

After this change, Choreographer should’ve warned me if my app skipped more than five frames at a time. However, in practice, the threshold remained the same 30 skipped frames.

I didn’t dig much into why this change didn’t affect Choreographer, but I suspect it’s related to the fact that Zygote process is initialized on device boot. Therefore, by the time I change this property, Zygote already set the threshold to the default 30 frames. And since all new apps’ processes are forks of Zygote, I can’t affect any of them either. Reboot could help, but system properties that start with “debug” get reset on reboot, so this change gets reverted. Too bad. So, even though it’s quite evident that the author of that code wanted this threshold to be configurable through this system property, I couldn’t get it to work.

Anyway, when something doesn’t work using the “officially recommended” approach in Android, we employ brute force. I mean reflection, of course.

This piece of code did the trick:

private void reduceChoreographerSkippedFramesWarningThreshold() {
    if (BuildConfig.DEBUG) {
        Field field = null;
        try {
            field = Choreographer.class.getDeclaredField("SKIPPED_FRAME_WARNING_LIMIT");
            field.setAccessible(true);
            field.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            field.set(null, 5);
        } catch (Throwable e) {
            Log.e(TAG, "failed to change choreographer's skipped frames threshold");
        }
    }
}

Call this method from your Application’s onCreate(), and you’ll be alright. I added DEBUG guard because with Google’s ongoing war on reflection I wouldn’t dare to ship this code to end users (didn’t even bother to check whether this call is in the list of approved exceptions; not worth the risk, IMO).

Reflective Access Denied

Unfortunately, as I suspected, reflective access to this field seems to be forbidden when your app targets higher versions of Android. As reported by one of the readers, if you use the above code, you might see an error similar to this:

Accessing hidden field Landroid/view/Choreographer;->SKIPPED_FRAME_WARNING_LIMIT:I (greylist-max-o, reflection, denied)

If that’s the case, then the only workaround I can think of is to lower the target API of your app for debug builds. Pretty sure not all projects will be able to do that, though.

Meanwhile, I submitted this request to open this API to Google’s issue tracker (issues requesting reflective access to APIs are hidden, so you won’t be able to vote for it):

[FEATURE/USE CASE]
Choreographer class has an internal private field called SKIPPED_FRAME_WARNING_LIMIT (https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/Choreographer.java#146). This field controls the number of skipped frames at which developers will start seeing warnings in logcat.

As per the commit message that introduced this functoinality into AOSP (https://github.com/aosp-mirror/platform_frameworks_base/commit/4fdf9c6e2a177845bb4cc20f69b83555de209144), SKIPPED_FRAME_WARNING_LIMIT was intended to be modified through “debug.choreographer.skipwarning” system property (see the source code).

However, in practice, this method doesn’t work. I suspect that this field is initialized in Zygote and then all new processes inherit it with a default value, such that changes to system properties can’t affect its value. And since “debug” system properties are reset on reboot, rebooting the device doesn’t help either.

[NON-SDK INTERFACES USED]
Accessing hidden field Landroid/view/Choreographer;->SKIPPED_FRAME_WARNING_LIMIT:I (greylist-max-o, reflection, denied)

[ALTERNATIVES CONSIDERED]
This is a “passive”, always-on watchdog for skipped frames. As far as I know, there are no alternatives to this functionality.
Systrace and gfxinfo, which also have functionality related to skipped frames, have different use cases and aren’t “passive” and “always-on”.

Let’s hope Google engineers will understand the importance of this feature and open reflective access to this field.

Edit: looks like there is a way to bypass the protection of APIs from reflective access with “double reflection”. Read Ligol’s comment under this post for details.

Dependency Injection with Dagger and Hilt Course

Learn Dependency Injection in Android and master Dagger and Hilt dependency injection frameworks.

Go to Course

Conclusion

In this article I discussed some basics of Android’s graphic user interface and I hope that now you understand why your application skips frames and what you can do about it.

In addition, you learned a handy trick to get earlier warnings about skipped frames. After I reduced the threshold to one skipped frame, for example, I discovered that all my apps, even the tutorials with simple UIs that I write for my courses, skip one-two frames here and there. Looks like it’s natural and unavoidable. Therefore, I decided to use a threshold of five frames to filter out the noise.

In my opinion, changing this threshold to get stricter (and, therefore, more reliable) monitoring of skipped frames would be a good practice in all Android projects.

Check out my premium

Android Development Courses

8 comments on "Why Android Applications Skip Frames and How to Fix This Issue"

  1. Hi Vasiliy,

    Do you know any tool that can help developers detected the exact part of the code where there is too much work on the ui thread? I work on an app that kind’ ve skips a lot of frames but I have a hard time detecting the exact part of the code in order to fix it.

    Thanks

    Reply
    • Hello Adrian,
      You’ll find this article very good in context of debugging UI performance. It’s a bit dated and some tools changed quite considerably, but the core part and the general ideas remained the same.

      Reply
  2. I work on a problem that seems rather easy. But not under Android.
    Java.
    I have to generate and write a large binary file.  Say 10-100M. It takes minutes.
    While generation and saving work fine. But not UI.

    I can not get progress to be shown on the device screen. Progress step, say a megabyte, takes several seconds.
    I tried to use ProgressBar or ProgressDialog
    as well as a simple TextView with modified text.
    Always 
    Choreographer: Skipped … frames!  The application may be doing too much work on its main thread.

    Neither AsyncTask nor runOnUiThread helps.

    What to do ?

    Reply
    • Hello Igor,
      Most probably, you’re executing some parts of this long-running flow on the UI thread. You need to offload this work to a background thread instead using one of the many mechanisms available in Android. I can’t help you with the specific implementation, so, if you’re still unsure how to proceed, I recommend asking a question on StackOverflow and attaching your code.

      Reply

Leave a Comment