Przemo
Przemo

Reputation: 538

Possible memory leak in NativeScript app if user reopens his app multiple times

I'm not sure where is the bug, maybe I'm using rxjs in a wrong way. ngDestroy is not working to unsubscribe observables in NativeScript if you want to close and back to your app. I tried to work with takeUntil, but with the same results. If the user close/open the app many times, it can cause a memory leak (if I understand the mobile environment correctly). Any ideas? This code below it's only a demo. I need to use users$ in many places in my app.

Tested with Android sdk emulator and on real device.

AppComponent

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription, Observable } from 'rxjs';
import { AppService } from './app.service';

import { AuthenticationService } from './authentication.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnDestroy, OnInit {
  public user$: Observable<any>;

  private subscriptions: Subscription[] = [];

  constructor(private appService: AppService, private authenticationService: AuthenticationService) {}

  public ngOnInit(): void {
    this.user$ = this.authenticationService.user$;

    this.subscriptions.push(
      this.authenticationService.user$.subscribe((user: any) => {
        console.log('user', !!user);
      })
    );
  }

  public ngOnDestroy(): void {
    if (this.subscriptions) {
      this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
    }
  }

  async signIn() {
    await this.appService.signIn();
  }

  async signOut() {
    await this.appService.signOut();
  }
}

AuthenticationService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { AppService } from './app.service';

@Injectable({
  providedIn: 'root',
})
export class AuthenticationService {
  public user$: Observable<any>;

  constructor(private appService: AppService) {
    this.user$ = this.appService.authState().pipe(shareReplay(1)); // I'm using this.users$ in many places in my app, so I need to use sharereplay
  }
}

AppService

import { Injectable, NgZone } from '@angular/core';
import { addAuthStateListener, login, LoginType, logout, User } from 'nativescript-plugin-firebase';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

const user$ = new BehaviorSubject<User>(null);

@Injectable({
  providedIn: 'root',
})
export class AppService {
  constructor(private ngZone: NgZone) {
    addAuthStateListener({
      onAuthStateChanged: ({ user }) => {
        this.ngZone.run(() => {
          user$.next(user);
        });
      },
    });
  }

  public authState(): Observable<User> {
    return user$.asObservable().pipe(distinctUntilChanged());
  }

  async signIn() {
    return await login({ type: LoginType.PASSWORD, passwordOptions: { email: 'xxx', password: 'xxx' } }).catch(
      (error: string) => {
        throw {
          message: error,
        };
      }
    );
  }

  signOut() {
    logout();
  }
}

Upvotes: 0

Views: 707

Answers (2)

Hypnotic
Hypnotic

Reputation: 21

One way to detect when the view comes from the background is to set callbacks on the router outlet (in angular will be)

<page-router-outlet
        (loaded)="outletLoaded($event)"
        (unloaded)="outletUnLoaded($event)"></page-router-outlet>

Then you cn use outletLoaded(args: EventData) {} to initialise your code respectively outletUnLoaded to destroy your subscriptions.

This is helpful in cases where you have access to the router outlet (in App Component for instance)

In case when you are somewhere inside the navigation tree you can listen for suspend event

Application.on(Application.suspendEvent, (data: EventData) => {
      this.backFromBackground = true;
    });

Then when opening the app if the flag is true it will give you a hint that you are coming from the background rather than opening for the first time.

It works pretty well for me. Hope that help you as well.

Upvotes: 0

Ian MacDonald
Ian MacDonald

Reputation: 14020

ngOnDestroy is called whenever a component is destroyed (following regular Angular workflow). If you have navigated forward in your app, previous views would still exist and would be unlikely to be destroyed.

If you are seeing multiple ngOnInit without any ngOnDestroy, then you have instantiated multiple components through some navigation, unrelated to your subscriptions. You should not expect the same instance of your component to be reused once ngOnDestroy has been called, so having a push to a Subscription[] array will only ever have one object.

If you are terminating the app (i.e. force quit swipe away), the whole JavaScript context is thrown out and memory is cleaned up. You won't run the risk of leaking outside of your app's context.

Incidentally, you're complicating your subscription tracking (and not just in the way that I described above about only ever having one pushed). A Subscription is an object that can have other Subscription objects attached for termination at the same time.

const subscription: Subscription = new Subscription();
subscription.add(interval(100).subscribe((n: number) => console.log(`first sub`));
subscription.add(interval(200).subscribe((n: number) => console.log(`second sub`));
subscription.add(interval(300).subscribe((n: number) => console.log(`third sub`));

timer(5000).subscribe(() => subscription.unsubscribe()); // terminates all added subscriptions

Be careful to add the subscribe call directly in .add and not with a closure. Annoyingly, this is exactly the same function call to make when you want to add a completion block to your subscription, passing a block instead:

subscription.add(() => console.log(`everybody's done.`));

Upvotes: 2

Related Questions