Skip to content

Commit 3f9f385

Browse files
zeyapmeta-codesync[bot]
authored andcommitted
Add an internal option to skip Native Animated frames while inactive (#58295)
Summary: Pull Request resolved: #58295 This is to investigate a crash on ios in C++ Animated rollout. On iOS, Native Animated drives frames from a `CADisplayLink` on the main run loop. While the app is inactive — which includes the whole `UIApplicationWillEnterForeground` → `UIApplicationDidBecomeActive` transition — it is not presenting, so a frame rendered then is never seen. Its commit and synchronous per-view updates still run on the main thread though, competing with the work the app must complete to become responsive. Adds `initWithSkipFramesDuringForegroundTransition:` to `RCTAnimatedModuleProvider`, declared in a new `RCTAnimatedModuleProvider+Private.h`. When YES, `_onDisplayLinkTick` returns early while `applicationState == UIApplicationStateInactive`. **The public API is unchanged.** `RCTAnimatedModuleProvider.h` is untouched and the C++ API snapshots have no delta — `+Private.h` is in the ReactApple `exclude_patterns`. Plain `init` still exists and defaults to NO, so every existing caller is unaffected; only hosts that opt in via the private header see different behaviour. Two properties worth being explicit about: - **The clock is not stopped, only the frame is skipped.** `AnimationDriver` computes progress from a timestamp (`timeDeltaMs = frameTimeMs - startFrameTimeMs_`), so the first frame after activation resolves to the value the animation should have reached rather than resuming from where it was suspended. - **Frames are not skipped while backgrounded** — `Background` is not `Inactive`. Completion handlers, and any app logic they drive, are therefore delayed by at most the length of the transition, not by the time spent in the background. Reading `applicationState` rather than tracking lifecycle notifications also avoids a failure mode: a mirrored flag must be cleared on every path out of the transition, including an abandoned foregrounding (a `willEnterForeground` with no following `didBecomeActive`), or frames are skipped indefinitely. There is no such state to get stuck here. `UIApplicationStateInactive` also covers other non-presenting moments — Control Center, the app switcher, an incoming call banner. Skipping frames there is harmless for the same reason: the clock keeps running and the first frame after activation is correct. The check sits inside the file's existing `TARGET_OS_OSX` guard, since `UIApplication` is iOS-only and this translation unit also builds for macOS. Changelog: [Internal] Reviewed By: javache, christophpurrer Differential Revision: D118188111 fbshipit-source-id: c9f19fafd7bc6f07649a7160fd71b02de888f879
1 parent d3b7893 commit 3f9f385

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#import <RCTAnimatedModuleProvider/RCTAnimatedModuleProvider.h>
11+
12+
@interface RCTAnimatedModuleProvider (Private)
13+
14+
/**
15+
* When true, animated frames are skipped while the application is inactive (between
16+
* `UIApplicationWillEnterForeground` -> `UIApplicationDidBecomeActive`), default is false.
17+
*/
18+
- (instancetype)initWithSkipFramesDuringForegroundTransition:(BOOL)skipFramesDuringForegroundTransition;
19+
20+
@end

packages/react-native/ReactApple/RCTAnimatedModuleProvider/RCTAnimatedModuleProvider.mm

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77

88
#import "RCTAnimatedModuleProvider.h"
99

10+
#import "RCTAnimatedModuleProvider+Private.h"
11+
1012
#import <functional>
1113

1214
#if TARGET_OS_OSX
1315
#import <React/RCTPlatformDisplayLink.h>
1416
#else
1517
#import <QuartzCore/CADisplayLink.h>
18+
#import <UIKit/UIKit.h>
1619
#endif
1720

1821
#import <react/featureflags/ReactNativeFeatureFlags.h>
@@ -28,6 +31,21 @@ @implementation RCTAnimatedModuleProvider {
2831
std::function<void()> _onRender;
2932

3033
std::weak_ptr<facebook::react::NativeAnimatedNodesManagerProvider> _nativeAnimatedNodesManagerProvider;
34+
35+
BOOL _skipFramesDuringForegroundTransition;
36+
}
37+
38+
- (instancetype)init
39+
{
40+
return [self initWithSkipFramesDuringForegroundTransition:NO];
41+
}
42+
43+
- (instancetype)initWithSkipFramesDuringForegroundTransition:(BOOL)skipFramesDuringForegroundTransition
44+
{
45+
if (self = [super init]) {
46+
_skipFramesDuringForegroundTransition = skipFramesDuringForegroundTransition;
47+
}
48+
return self;
3149
}
3250

3351
- (void)dealloc
@@ -61,6 +79,15 @@ - (void)_onDisplayLinkTick
6179
// use-after-free during hot reload. The provider must remain alive for the
6280
// entire duration of _onRender() since it holds references to the animation
6381
// nodes manager and related data structures.
82+
#if !TARGET_OS_OSX
83+
// See initWithSkipFramesDuringForegroundTransition: the frame is skipped,
84+
// never the clock.
85+
if (_skipFramesDuringForegroundTransition &&
86+
UIApplication.sharedApplication.applicationState == UIApplicationStateInactive) {
87+
return;
88+
}
89+
#endif
90+
6491
auto strongProvider = _nativeAnimatedNodesManagerProvider.lock();
6592
if (strongProvider != nullptr && _displayLink != nullptr && _onRender != nullptr) {
6693
_onRender();

0 commit comments

Comments
 (0)