Skip to content

Memory leak (retain cycle) in [FIRQuery getDocumentsWithSource:completion:] #2564

@ewanmellor

Description

@ewanmellor

[REQUIRED] Step 2: Describe your environment

  • Xcode version: 10.1 (10B61)
  • Firebase SDK version: 5.16.0
  • Firebase Component: Firestore (Auth, Core, Database, Firestore, Messaging, Storage, etc)
  • Component version: Firebase/Firestore (5.16.0), FirebaseFirestore (= 1.0.0) from CocoaPods

[REQUIRED] Step 3: Describe the problem

There is a retain cycle identified by Xcode Instruments as shown below.

From Firestore/Source/API/FIRQuery.mm:113:

- (void)getDocumentsWithSource:(FIRFirestoreSource)source
                    completion:(void (^)(FIRQuerySnapshot *_Nullable snapshot,
                                         NSError *_Nullable error))completion {

  // [Snip]

  __block id<FIRListenerRegistration> listenerRegistration;
  FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) {
    if (error) {
      completion(nil, error);
      return;
    }

    // Remove query first before passing event to user to avoid user actions affecting the
    // now stale query.
    dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER);
    [listenerRegistration remove];

    if (snapshot.metadata.fromCache && source == FIRFirestoreSourceServer) {
      completion(nil,  /* [Snip NSError] */);
    } else {
      completion(snapshot, nil);
    }
  };

  listenerRegistration = [self addSnapshotListenerInternalWithOptions:listenOptions
                                                             listener:listener];
  dispatch_semaphore_signal(registered);
}

My reading is as follows:

  1. FIRQuerySnapshotBlock listener is a callback block that is given to addSnapshotListenerInternalWithOptions.
  2. That callback block references __block id<FIRListenerRegistration> listenerRegistration.
  3. listenerRegistration is the result of the addSnapshotListenerInternalWithOptions call, which ultimately contains a reference to listener.
  4. Nothing ever nils listenerRegistration, so there is a retain cycle and a leak once the callback is complete.

I don't know enough about the code here to be sure, but my guess is that setting listenerRegistration = nil after each of the three completion() calls would fix the problem.

Screen Shot 2019-03-14 at 11 48 58 PM

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions