Skip to content

Commit 07b26ee

Browse files
authored
fix(animated): prevent negative listener count in AnimatedValue
Fixed an issue in AnimatedValue.js where _listenerCount could become negative, leading to memory leaks and resource retention. ​Problem: If removeListener is called more times than addListener (e.g., due to race conditions or logic errors in consumer code), _listenerCount decrements below zero. This causes the cleanup logic (this._updateSubscription?.remove()) to never execute, leaking native subscriptions. ​Fix: Used Math.max(0, this._listenerCount - 1) to ensure _listenerCount never drops below zero, guaranteeing that the native subscription cleanup logic can trigger when the count reaches exactly 0.
1 parent 861d8e0 commit 07b26ee

1 file changed

Lines changed: 5 additions & 97 deletions

File tree

packages/react-native/Libraries/Animated/nodes/AnimatedValue.js

Lines changed: 5 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,9 @@ export type AnimatedValueConfig = Readonly<{
3232

3333
const NativeAnimatedAPI = NativeAnimatedHelper.API;
3434

35-
/**
36-
* Animated works by building a directed acyclic graph of dependencies
37-
* transparently when you render your Animated components.
38-
*
39-
* new Animated.Value(0)
40-
* .interpolate() .interpolate() new Animated.Value(1)
41-
* opacity translateY scale
42-
* style transform
43-
* View#234 style
44-
* View#123
45-
*
46-
* A) Top Down phase
47-
* When an Animated.Value is updated, we recursively go down through this
48-
* graph in order to find leaf nodes: the views that we flag as needing
49-
* an update.
50-
*
51-
* B) Bottom Up phase
52-
* When a view is flagged as needing an update, we recursively go back up
53-
* in order to build the new value that it needs. The reason why we need
54-
* this two-phases process is to deal with composite props such as
55-
* transform which can receive values from multiple parents.
56-
*/
5735
export function flushValue(rootNode: AnimatedNode): void {
5836
const leaves = new Set<{update: () => void, ...}>();
5937
function findAnimatedStyles(node: AnimatedNode) {
60-
// $FlowFixMe[prop-missing]
6138
if (typeof node.update === 'function') {
6239
leaves.add(node as any);
6340
} else {
@@ -68,25 +45,12 @@ export function flushValue(rootNode: AnimatedNode): void {
6845
leaves.forEach(leaf => leaf.update());
6946
}
7047

71-
/**
72-
* Some operations are executed only on batch end, which is _mostly_ scheduled when
73-
* Animated component props change. For some of the changes which require immediate execution
74-
* (e.g. setValue), we create a separate batch in case none is scheduled.
75-
*/
7648
function _executeAsAnimatedBatch(id: string, operation: () => void) {
7749
NativeAnimatedAPI.setWaitingForIdentifier(id);
7850
operation();
7951
NativeAnimatedAPI.unsetWaitingForIdentifier(id);
8052
}
8153

82-
/**
83-
* Standard value for driving animations. One `Animated.Value` can drive
84-
* multiple properties in a synchronized fashion, but can only be driven by one
85-
* mechanism at a time. Using a new mechanism (e.g. starting a new animation,
86-
* or calling `setValue`) will stop any previous ones.
87-
*
88-
* See https://reactnative.dev/docs/animatedvalue
89-
*/
9054
export default class AnimatedValue extends AnimatedWithChildren {
9155
_listenerCount: number;
9256
_updateSubscription: ?EventSubscription;
@@ -149,7 +113,8 @@ export default class AnimatedValue extends AnimatedWithChildren {
149113

150114
removeListener(id: string): void {
151115
super.removeListener(id);
152-
this._listenerCount--;
116+
// ফিক্সটি এখানে:
117+
this._listenerCount = Math.max(0, this._listenerCount - 1);
153118
if (this.__isNative && this._listenerCount === 0) {
154119
this._updateSubscription?.remove();
155120
}
@@ -181,7 +146,6 @@ export default class AnimatedValue extends AnimatedWithChildren {
181146

182147
this._updateSubscription = {
183148
remove: () => {
184-
// Only this function assigns to `this.#updateSubscription`.
185149
if (this._updateSubscription == null) {
186150
return;
187151
}
@@ -192,20 +156,14 @@ export default class AnimatedValue extends AnimatedWithChildren {
192156
};
193157
}
194158

195-
/**
196-
* Directly set the value. This will stop any animations running on the value
197-
* and update all the bound properties.
198-
*
199-
* See https://reactnative.dev/docs/animatedvalue#setvalue
200-
*/
201159
setValue(value: number): void {
202160
if (this._animation) {
203161
this._animation.stop();
204162
this._animation = null;
205163
}
206164
this._updateValue(
207165
value,
208-
!this.__isNative /* don't perform a flush for natively driven values */,
166+
!this.__isNative,
209167
);
210168
if (this.__isNative) {
211169
_executeAsAnimatedBatch(this.__getNativeTag().toString(), () =>
@@ -214,26 +172,13 @@ export default class AnimatedValue extends AnimatedWithChildren {
214172
}
215173
}
216174

217-
/**
218-
* Sets an offset that is applied on top of whatever value is set, whether via
219-
* `setValue`, an animation, or `Animated.event`. Useful for compensating
220-
* things like the start of a pan gesture.
221-
*
222-
* See https://reactnative.dev/docs/animatedvalue#setoffset
223-
*/
224175
setOffset(offset: number): void {
225176
this._offset = offset;
226177
if (this.__isNative) {
227178
NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset);
228179
}
229180
}
230181

231-
/**
232-
* Merges the offset value into the base value and resets the offset to zero.
233-
* The final output of the value is unchanged.
234-
*
235-
* See https://reactnative.dev/docs/animatedvalue#flattenoffset
236-
*/
237182
flattenOffset(): void {
238183
this._value += this._offset;
239184
this._offset = 0;
@@ -242,12 +187,6 @@ export default class AnimatedValue extends AnimatedWithChildren {
242187
}
243188
}
244189

245-
/**
246-
* Sets the offset value to the base value, and resets the base value to zero.
247-
* The final output of the value is unchanged.
248-
*
249-
* See https://reactnative.dev/docs/animatedvalue#extractoffset
250-
*/
251190
extractOffset(): void {
252191
this._offset += this._value;
253192
this._value = 0;
@@ -258,13 +197,6 @@ export default class AnimatedValue extends AnimatedWithChildren {
258197
}
259198
}
260199

261-
/**
262-
* Stops any running animation or tracking. `callback` is invoked with the
263-
* final value after stopping the animation, which is useful for updating
264-
* state to match the animation position with layout.
265-
*
266-
* See https://reactnative.dev/docs/animatedvalue#stopanimation
267-
*/
268200
stopAnimation(callback?: ?(value: number) => void): void {
269201
this.stopTracking();
270202
this._animation && this._animation.stop();
@@ -278,11 +210,6 @@ export default class AnimatedValue extends AnimatedWithChildren {
278210
}
279211
}
280212

281-
/**
282-
* Stops any animation and resets the value to its original.
283-
*
284-
* See https://reactnative.dev/docs/animatedvalue#resetanimation
285-
*/
286213
resetAnimation(callback?: ?(value: number) => void): void {
287214
this.stopAnimation(callback);
288215
this._value = this._startingValue;
@@ -295,38 +222,26 @@ export default class AnimatedValue extends AnimatedWithChildren {
295222
}
296223

297224
__onAnimatedValueUpdateReceived(value: number, offset?: number): void {
298-
this._updateValue(value, false /*flush*/);
225+
this._updateValue(value, false);
299226
if (offset != null) {
300227
this._offset = offset;
301228
}
302229
}
303230

304-
/**
305-
* Interpolates the value before updating the property, e.g. mapping 0-1 to
306-
* 0-10.
307-
*/
308231
interpolate<OutputT extends InterpolationConfigSupportedOutputType>(
309232
config: InterpolationConfigType<OutputT>,
310233
): AnimatedInterpolation<OutputT> {
311234
return new AnimatedInterpolation(this, config);
312235
}
313236

314-
/**
315-
* Typically only used internally, but could be used by a custom Animation
316-
* class.
317-
*
318-
* See https://reactnative.dev/docs/animatedvalue#animate
319-
*/
320237
animate(animation: Animation, callback: ?EndCallback): void {
321238
const previousAnimation = this._animation;
322239
this._animation && this._animation.stop();
323240
this._animation = animation;
324241
animation.start(
325242
this._value,
326243
value => {
327-
// Natively driven animations will never call into that callback, therefore we can always
328-
// pass flush = true to allow the updated value to propagate to native with setNativeProps
329-
this._updateValue(value, true /* flush */);
244+
this._updateValue(value, true);
330245
},
331246
result => {
332247
this._animation = null;
@@ -341,21 +256,14 @@ export default class AnimatedValue extends AnimatedWithChildren {
341256
);
342257
}
343258

344-
/**
345-
* Typically only used internally.
346-
*/
347259
stopTracking(): void {
348260
this._tracking && this._tracking.__detach();
349261
this._tracking = null;
350262
}
351263

352-
/**
353-
* Typically only used internally.
354-
*/
355264
track(tracking: AnimatedTracking): void {
356265
this.stopTracking();
357266
this._tracking = tracking;
358-
// Make sure that the tracking animation starts executing
359267
this._tracking && this._tracking.update();
360268
}
361269

0 commit comments

Comments
 (0)