Skip to content

[iOS ANR]: iOS white/black screen after long background (UIScene + Impeller) #183814

@kwongfung

Description

@kwongfung

Summary

On iOS, when the app has been in the background for a long time (e.g. 30+ minutes, or after prolonged music playback in another app), returning to the app often results in a white or black screen with no Flutter UI. The process is running and the system reports the app as visible and active, but nothing is rendered. Force-quitting and relaunching usually fixes it until the next long background.

Environment

  • Flutter: 3.38.9 (stable)
  • Platform: iOS (physical device; issue is rare or absent on simulator)
  • Architecture: Standard Flutter app with UIScene lifecycle (SceneDelegate), single window
  • Rendering: Impeller (default on iOS since 3.29; Skia not available)
  • Reproduction: More likely after long background and/or when another app (e.g. music) has been in foreground

Steps to reproduce

  1. Launch the Flutter app on a physical iOS device.
  2. Send the app to background (home or app switcher).
  3. Leave the device with the app in background for a long time (e.g. 30+ minutes), optionally with another app (e.g. music) in foreground.
  4. Bring the Flutter app back to foreground (tap icon or from app switcher).

Result: Screen stays white (or black). No Flutter content is drawn. App does not crash; it just fails to render.

Expected: App should show the last screen or the appropriate UI after resume.

Observations from system logs

  • SpringBoard / runningboard report the app as running-active, Visible, and UserInteractiveFocal.
  • Scene lifecycle reaches Foreground and scene content state becomes ready.
  • Runner (the app process) sometimes reports itself as unknown-NotVisible or running-active-NotVisible in RBS/state updates while the system considers the app visible—suggesting an internal visibility or rendering-pipeline mismatch.
  • Logs often contain: Returning cached initialization context (warm/resume path) and sometimes updated settings were not derived from the scene's previous settings.
  • No Flutter or engine crash; the failure is “no frame painted” rather than a crash.

Suspected root cause (for implementers)

We believe this is related to Impeller/Metal surface lifecycle after long background:

  1. Surface invalidation: After long background, iOS may reclaim or invalidate GPU/Metal resources (CAMetalLayer, drawables, or context). The Flutter engine’s Metal surface may no longer be valid.
  2. No automatic surface recreation: Engine creates the surface in viewDidLayoutSubviews when the app is active. If the view’s bounds do not change on resume, layout may not run again, so the engine never recreates the surface and keeps using an invalid one → nothing is drawn.
  3. Frame vs. surface timing: When the app becomes active, Dart receives resumed and typically calls scheduleFrame() once. If the Metal surface is recreated asynchronously and the first frame is requested before the new surface is ready, that frame can be dropped and no further frame is scheduled → prolonged white/black screen.

So the issue likely combines: (a) surface invalidation after long background, (b) no bounds change so no viewDidLayoutSubviews-driven surface recreation, and (c) first frame scheduled too early relative to surface readiness.

Workarounds we use (app-side)

We cannot fix the engine from the app; the following only mitigate:

  1. Force surface invalidation (SceneDelegate): On sceneWillEnterForeground and sceneDidBecomeActive, we temporarily change the Flutter view’s frame (e.g. insetBy(dx: 0, dy: 0.5) then restore) so the engine sees a bounds change and recreates the surface in viewDidLayoutSubviews. We repeat this at 0.3s, 0.6s, 1s, 2s, 3s to cover slow GPU recovery.
  2. Multiple delayed scheduleFrame() (Dart): In WidgetsBindingObserver.didChangeAppLifecycleState(resumed), we call WidgetsBinding.instance.scheduleFrame() immediately and again at 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000 ms so that at least one frame request happens after the new surface is ready.
  3. runApp first: We avoid blocking runApp() on async init (e.g. Firebase, Prefs) so the first frame is scheduled as soon as possible; heavy init runs after the first frame.
  4. FLTDisablePartialRepaint (Info.plist): We set this to YES as a possible mitigation for partial-repaint-related glitches; it may or may not help this specific issue.

These workarounds reduce but do not fully eliminate the problem, especially after very long background (e.g. hours) or under memory pressure.

What we are asking for

  • Engine / framework: Ensure that when the app returns to foreground after long background, the Impeller/Metal surface is recreated or revalidated (e.g. in response to scene activation or a guaranteed layout pass), and that at least one frame is scheduled after the new surface is ready, so that Flutter UI is drawn without app-side hacks.
  • Documentation: If there are recommended patterns or constraints for UIScene + Impeller on iOS (e.g. when to trigger layout, or how to avoid this scenario), documenting them would help.

Related issues / PRs (if any)

Checklist

  • I have read the contributing guide.
  • I have searched existing issues and discussions; this appears to be a known class of problem but we are providing a concrete repro and environment.
  • I am willing to provide more logs or a minimal repro if requested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projecte: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-iosiOS applications specificallyteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions