CN

Analysis of 'Zombie Animations' in the Android Background

Word count: 1.5kReading time: 9 min
2019/10/24
loading

This article records a 2019 issue: animation work continued after the app’s UI was no longer visible. However, some apps continue to run “zombie animations”—animations that are completely invisible to the user yet consume precious CPU cycles and battery. When users discover this, the result is often a manual kill, an OS-level background restriction, or an outright uninstallation.

This issue can be easy to miss. In the original investigation, I used Systrace after opening apps and returning home, capturing traces while swiping between launcher pages. Some departed apps still fired callbacks. A reproduction should record versions, operations, and visibility; opening many apps is not itself a diagnostic requirement or proof.

Here, “zombie animations” means updates that continue when their UI is invisible and the feature no longer needs them. Many paths can schedule animation work through Choreographer. Investigate the actual stack, playback state, and library version. A live process is not necessarily free to consume CPU indefinitely; modern systems can freeze eligible cached processes.

Version note — technical review, 2026-10-13: These cases and screenshots are from 2019, not tests of current NetEase News, QQ Music, or Lottie versions. Updates below cover Lottie Android 6.7.1, lifecycle/window visibility, and the cached-app freezer supported from Android 11 / API 30. Complete original traces, app versions, and power measurements are unavailable; CPU screenshots are not battery measurements.

Let’s look at two real-world examples to understand the technical cause and the fix. If you are a developer, I highly recommend checking your app for this behavior—fix it if it’s there, celebrate if it’s not.

Case 1: NetEase News

At the time, I used NetEase News, returned home, and captured a Systrace while swiping the launcher. I could see that the app’s UI had left the foreground. The screenshot directly shows repeated doFrame activity in com.netease.newsreader.activity, PID 30962:

Repeated doFrame activity in the historical NetEase News process

The selected frame contains animation work without visible input/traversal intervals. That alone does not establish background state: a foreground frame with no input or View updates can look similar. Chrome-related tracks are also present; not all process activity can be assigned to one animation library.

Selected animation callback and Chrome-related activity

The CPU Slices selection spans about 3.636 s. Its reader.activity row totals 422.163 ms over 680 execution slices and appears first in the visible ranking. Those 680 occurrences are not animations, callback invocations, or dropped frames. A name-based grouping also needs thread/process attribution. CPU time is not a direct energy measurement, and the whole row cannot automatically be attributed to one animation callback.

reader.activity totals 422.163ms across 680 CPU slices in a 3.636s selection

I then checked MethodTrace and found ValueAnimator entering com.airbnb.lottie...onAnimationUpdate, updating layers and issuing invalidations. This identifies a Lottie update path to investigate. It supplies no exact library version, object identity, or visibility state, and cannot establish a defect in every Lottie version. Instrumentation also affects timing.

Historical ValueAnimator to Lottie onAnimationUpdate call stack

Case 2: QQ Music

Next I tried QQ Music: launch it, return home, and capture while swiping the launcher again. The historical com.tencent.qqmusic process, PID 19330, shows repeated frame activity:

Repeated frame activity in the historical QQ Music process

The method trace contains three ValueAnimator/Lottie update branches within one AnimationHandler interval. Multiple updates merit investigation, but without object identities the branches do not prove exactly three distinct animation instances. They also do not establish a more severe battery impact than another app.

Three ValueAnimator and Lottie update branches under AnimationHandler

The zoomed 10.73 ms belongs to the selected obfuscated com.airbnb.lottie.aw.b method in this instrumented recording. It is not a power figure or an unqualified production-cost measurement.

A selected historical Lottie method interval of 10.73ms under instrumentation

Ordinary property animations can exhibit unwanted continued work too. The next stack includes ObjectAnimator, View.setRotation, invalidation, and later TraversalRunnable/performTraversals. It contradicts a universal rule that such animations never trigger traversal.

Property animation updates rotation and triggers invalidation and traversal

Root Cause

The investigation is about animation work still being requested and advanced after its UI may be invisible or its feature no longer needs it. Distinguish missing app stop intent, historical library visibility handling, ancestor/window changes, recycling, and late asynchronous loading that restarts work.

Choreographer runs registered callbacks; it does not uniformly delete all input/draw work while retaining animation merely because an app is backgrounded. No traversal slice only means no corresponding View work was observed there, not that every Surface, WebView, or native renderer has stopped. Background audio playback in QQ Music is a separate function: pausing decorative animation does not mean stopping all background work.

Modern system boundary: Android 11 / API 30 supports the cached-app freezer. On supported, enabled devices, eligible cached processes cannot perform CPU work while frozen. Not every background process immediately reaches that state, and freezing does not replace correct playback-state management. Record process state rather than equating existence, invisibility, and execution.

Development Recommendation

The original report was Lottie issue #987, opened 2018-10-31 and closed 2019-02-03. It described looping animations continuing while the View was hidden or the Activity paused, and questioned cleanup based only on detachment. This is a historical report; its closure alone does not prove every version or integration is fixed.

Do not copy the old onVisibilityChanged() subclass patch blindly. It omitted the state field and could clear a remembered resume flag on a second invisible notification. The visibility parameter alone also cannot represent host lifecycle, ancestor/window state, or a user’s explicit pause. Adding it over the library’s own state handling can duplicate control.

What Lottie 6.7.1 Already Handles

This review checked release 6.7.1, pinned to commit c8addcb776f6cd940abaf55ae6b471e55b626eb9:

  • LottieDrawable.setVisible() pauses a running animator when becoming invisible and remembers a later resume action. It explicitly avoids erasing that intent on repeated invisible notifications.
  • LottieAnimationView uses View/ImageView visibility propagation. Its pauseAnimation() delegates to the Drawable and disables autoplay; current handling is not limited to the old report’s detach cleanup.
  • LottieValueAnimator.pauseAnimation() removes its Choreographer callback. Drawable pause also clears deferred composition tasks and, while invisible, pending resume actions. Not currently running does not always mean no pending playback request.

These are implementation facts for the checked version, not a guarantee that upgrading makes every integration correct. Visibility propagation also depends on system and target-SDK compatibility paths; see Android 16 ImageView. Standalone Drawables, custom containers, Compose, and other versions require checking their own integration paths.

Separate Playback Intent from Temporary Suspension

Pausing an unneeded, invisible decorative animation is usually appropriate. Do not mechanically stop everything in onPause and restart everything in onResume: a paused Activity may remain visible in multi-window. Visibility-based work commonly follows onStart/onStop; Fragment work should follow its View lifecycle and release references/listeners when that View is destroyed.

Use one owner to decide whether animation should continue:

  1. Does the feature/user still request playback, or has the animation completed or been cancelled?
  2. Do the host lifecycle, View/ancestor/window visibility, and attachment state permit it? isShown() alone does not prove actual pixel visibility or detect every occlusion.
  3. Is the composition ready? Re-evaluate after loading, recycling, and navigation so obsolete load callbacks cannot revive unwanted playback.

This is a function that applies an existing decision, not a complete lifecycle component. Compute shouldAnimate from those states and update it when they change. Natural completion must also clear playback intent so a later update does not restart a finished one-shot animation:

1
2
3
4
5
6
7
8
9
// Call on the main thread whenever the owning screen's playback policy changes.
private void syncAnimationPlayback(LottieAnimationView view, boolean shouldAnimate) {
if (!shouldAnimate) {
// Also clear a pending play/resume request, even if not currently running.
view.pauseAnimation();
} else if (!view.isAnimating()) {
view.resumeAnimation();
}
}

pauseAnimation() retains the current position; resumeAnimation() continues from it. Starting over, completion, and cancellation are distinct product decisions. Do not use isAnimating() as the sole representation of intent or call resume unconditionally. Use the main thread and verify the installed Lottie version’s event semantics.

Verifying the Fix

Fix app/library/system versions and test Home, navigation, ancestor hiding, window invisibility, list recycling, and composition loading that completes in the background. Returning to visibility should resume only what is still requested; user-paused or completed animations must stay stopped. Test multi-window and picture-in-picture according to the product’s rules.

Compare the target thread’s scheduled CPU execution, animation stacks, and callback counts over equivalent operations and intervals. Confirm that unwanted self-reposting stops; evaluate energy separately with controlled power measurements. No animation slice, an uncaptured thread, or a frozen process alone does not prove that lifecycle handling is correct.

Article Discussion

Original Chinese discussion pages: Zhihu and Juejin.

About Me && Blog

I welcome discussion with fellow developers:

  1. About me: personal introduction and contact information.
  2. Blog navigation.
  3. Android performance resources: recommendations are welcome.
  4. Android performance community: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.

One person can go faster; a group can go further.

WeChat QR code

CATALOG
  1. 1. Case 1: NetEase News
  2. 2. Case 2: QQ Music
  3. 3. Root Cause
  4. 4. Development Recommendation
    1. 4.1. What Lottie 6.7.1 Already Handles
    2. 4.2. Separate Playback Intent from Temporary Suspension
    3. 4.3. Verifying the Fix
  5. 5. Article Discussion
  6. 6. About Me && Blog