Problem with "Zombie pages" and MessagingCenter subscribers that keep living after unsubscribe

LapsusLapsus USMember ✭✭

Hi everyone,

I'm having a bit of an issue with so called "zombie" pages and messaging center subscribers.
Let's say my application is structured as follows: LoginPage -> MenuPage -> PageA -> PageB

In each of the PageA and PageB I subscribe to the messaging center messages in the OnAppearing method and then unsubscribe in the OnDisappearing method, since I only want to listen to the messages when the page is active (displayed). If I navigate directly from page to page everything is working correctly.

However I am checking the user session when OnAppearing of each of the PageA and PageB is called, and if the user session has expired, I return the user to the first page (LoginPage). In order to achieve this, I created a helper method in my App class, which replaced the current MainPage (as was suggested on other threads on this page):

public void ClearNavigation()
{
    MainPage = new NavigationPage(new Login());
}

So for example if I call the ClearNavigation in my PageB page, the OnDisappearing method will be called and with it the unsubscribe call to the message. After that, the LoginPage will be shown. I'm checking the NavigationStack size, and it is 1, so everything is looking ok.

However:
If I trigger a message, that each of the pages is subscribing/unsubscribing to, something really weird will happen. The message will get to the PageB (it will actually trigger a breakpoint in the PageB), which is not in the current NavigationStack, and was unsubscribed in the OnDisappearing method (and OnAppearing was not called after that).

So 2 things are really strange here:
1. The page still lives in memory, after the MainPage was replaced. In fact all pages still live in the memory, since if I check the NavigationStack of the page which receives the message, it's different than the one I'm currently on (the one with only 1 page) I'm guessing this is not the correct way to reset your application in that case...)? I tried using the .PopToRootAsync() method, but that usually results into exception and Application crash.
2. The message is hitting it's subscriber, even if it was unsubscribed (and not subscribed again). So it looks like the OnDisappearing does not finish executing in the event that the MainPage is replaced, or something?)

Does anyone has any similar experience or would be able to give me a insight into what is causing this strange and unwanted behavior? Is this normal behaviour or maybe an Xamarin.Forms bug? I am using the version 3.6, but the problem was the same in the 3.5, I didn't test any other versions.

BR,
Denis

Best Answers

  • LapsusLapsus US ✭✭
    edited March 29 Accepted Answer

    @Lapsus said:

    @RHudson said:
    What if you change the message signature after the user logs in?

    instead of

    MessagingCenter.Send<MainPage, string> (this, "Hi", "John");
    

    As a test try this, do the pages still get confused?

    var guid = Guid.NewGuid();
    MessagingCenter.Send<MainPage, string> (this, guid, "John");
    

    Show your code, how are you sub/unsubscribing?

    Also... is it an option for you to use plain old C# events instead of MessagingCenter?

    The unsubscribing is working, since if I navigate from PageA with a simple Navigation.PopAsync, the message will no longer hit the page, even though it is still living in memory.

    //This how I subscribe in the OnAppearing of PageA
    MessagingCenter.Unsubscribe<string>(this, TopicEnum.BARCODE.ToString());
    MessagingCenter.Subscribe<string>(this, TopicEnum.BARCODE.ToString(), async (arg) => {
        await ParseScanResult(arg, true);
    });
    
    //And this is how I unsubscribe in my OnDisappearing of PageA
    MessagingCenter.Unsubscribe<string>(this, TopicEnum.BARCODE.ToString());
    

    I think I finally figured out this, it turns out the error was caused by my error of assuming that Xamarin.Forms is working somewhat differently as it apparently is. The leak was indeed caused by the remaining subscriptions to the MessagingCenter from the "disposed"pages. But how did those subscriptions came to life, if I subscribe in the OnAppearing and unsubscribe in the OnDisappearing? Well my code is structured like this:

    protected override void OnAppearing()
    {
        base.OnAppearing();
        //Check user session
        //Subscribe to MessagingCenter
    }
    
    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        //Unsubscribe from MessagingCenter
    }
    

    And in my "Check user session" I do the replacing of the MainPage, if the session is marked as expired. Now my wrong assumption comes into play: I assumed that in that case, the OnDisappearing will fire after OnAppearing has finished it's execution since probably pages live in some kind of a state machine. Well what happens is not like I assumed, but instead the replacing of the MainPage causes the OnDisappearing to fire instantly and only when OnDisappearing finishes, the OnAppearing execution will resume. This of course caused the subscribe calls to be called AFTER the unsubscribe calls thus resulting in the live subscriptions, that prevented the pages from being collected by the GC and to remain listening to the messaging center messages. I solved this so now I check the status of the CheckUserSession call, and if session has expired, I break the execution of the OnAppearing from that point.

    I think I will really need to create a wrapper for the MessagingCenter where I will keep the track of all active subscriptions, since this is the second time in my XF journey that the MessagingCenter got the better part of me :)

    Thank you both for your help, I don't think I would figure this out in any reasonable time, without digging around in the Xamarin Profiler, and double checking the MessagingCenter subscriptions and removal of them.

Answers

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    Just killing pages like that is no guarantee that they will survive long enough to complete their methods.
    Do you have your unsubscribe calls in the page destructors as well... so if they get destroyed abruptly they can unsubscribe?

  • LapsusLapsus USMember ✭✭

    @ClintStLaurent said:
    Just killing pages like that is no guarantee that they will survive long enough to complete their methods.
    Do you have your unsubscribe calls in the page destructors as well... so if they get destroyed abruptly they can unsubscribe?

    Hey, I tried putting a breakpoint in the destructor/finalizer, but the breakpoint never fires, so I'm guessing the destructor is not beeing called..

  • JohnHardmanJohnHardman GBUniversity mod
    edited March 28

    @Lapsus said:

    @ClintStLaurent said:
    Just killing pages like that is no guarantee that they will survive long enough to complete their methods.
    Do you have your unsubscribe calls in the page destructors as well... so if they get destroyed abruptly they can unsubscribe?

    Hey, I tried putting a breakpoint in the destructor/finalizer, but the breakpoint never fires, so I'm guessing the destructor is not beeing called..

    See https://forums.xamarin.com/discussion/comment/369353/#Comment_369353 for why :-)

  • LapsusLapsus USMember ✭✭

    @RHudson said:
    @Lapsus Why are you making the individual pages responsible for navigation? The pages shouldn't need to care about the session state.

    In the App class, subscribe to your user session service. When it detects an expiration, simply call

    MainPage = new NavigationPage(new Login());
    

    Hey, I accepted this by mistake. I don't get why this would solve my problem, since I would to the same

    MainPage = new NavigationPage(new Login());
    

    which is causing me problems. What would be the difference in where I call the following code that is in the App.cs?

  • LapsusLapsus USMember ✭✭

    @JohnHardman said:
    Sounds like a leak that is preventing the page from being finalised. Something is holding a reference to the page or something on the page. Does PageA keep a reference to PageB? Have you definitely unsubscribed from MessagingCenter (I assume not)? Have you created Commands that reference the page, or created event handlers that reference the page etc? If you have access to the Xamarin profiler, you can use that to help identify what is holding the reference. Otherwise, you might want to build a poor-person's equivalent that you can enable or disable at build time - this is what I do.

    Hey, I'm guessing that there is indeed a memory leak in my pages. I have definitely unsubscribed from MessagingCenter (I always make sure, that I add unsubscribe call to the OnDisappearing as soon as I subscribe in the OnAppearing, since I had some problems with rougue subscriptions in the past and I have learned my lesson :))

    What I noticed is that I'm not actually taking care of the event subscriptions for the UI elements, for example I set the EventHandler for the Entry.Focused and Entry.Unfocused, but I never remove this subscriptions, could this be the issue and how do I properly dispose of the subscriptions? Should I move everything from the constructor in OnAppearing/OnDisappearing?

    I do have access to the Xamarin Profiler, but for some reason it is not working at the moment, so I'm guessing this will be the first thing I need to solve. How are you checking for memoryleaks in your "poor-person" equivalent for this?

  • RHudsonRHudson CAMember ✭✭✭

    @Lapsus said:

    @RHudson said:
    @Lapsus Why are you making the individual pages responsible for navigation? The pages shouldn't need to care about the session state.

    In the App class, subscribe to your user session service. When it detects an expiration, simply call

    MainPage = new NavigationPage(new Login());
    

    Hey, I accepted this by mistake. I don't get why this would solve my problem, since I would to the same

    MainPage = new NavigationPage(new Login());
    

    which is causing me problems. What would be the difference in where I call the following code that is in the App.cs?

    Remove the MessagagingCenter subscriptions from PageA, PageB etc. altogether.

    Why are the pages subscribing to the MessagingCenter?

  • LapsusLapsus USMember ✭✭

    @RHudson said:

    @Lapsus said:

    @RHudson said:
    @Lapsus Why are you making the individual pages responsible for navigation? The pages shouldn't need to care about the session state.

    In the App class, subscribe to your user session service. When it detects an expiration, simply call

    MainPage = new NavigationPage(new Login());
    

    Hey, I accepted this by mistake. I don't get why this would solve my problem, since I would to the same

    MainPage = new NavigationPage(new Login());
    

    which is causing me problems. What would be the difference in where I call the following code that is in the App.cs?

    Remove the MessagagingCenter subscriptions from PageA, PageB etc. altogether.

    Why are the pages subscribing to the MessagingCenter?

    Because they are receiving events from the native layer. For example physical buttons presses and events from different HW (like barcode scanner, compass, ...)

  • RHudsonRHudson CAMember ✭✭✭
    edited March 28

    What if you change the message signature after the user logs in?

    instead of

    MessagingCenter.Send<MainPage, string> (this, "Hi", "John");
    

    As a test try this, do the pages still get confused?

    var guid = Guid.NewGuid();
    MessagingCenter.Send<MainPage, string> (this, guid, "John");
    

    Show your code, how are you sub/unsubscribing?

    Also... is it an option for you to use plain old C# events instead of MessagingCenter?

  • LapsusLapsus USMember ✭✭

    @JohnHardman said:

    @Lapsus said:
    What I noticed is that I'm not actually taking care of the event subscriptions for the UI elements, for example I set the EventHandler for the Entry.Focused and Entry.Unfocused, but I never remove this subscriptions, could this be the issue and how do I properly dispose of the subscriptions? Should I move everything from the constructor in OnAppearing/OnDisappearing?

    Adding event handlers will normally be done in OnAppearing
    Removing event handlers will normally be done in OnDisappearing

    (obviously, if you need to receive events even when another page is pushed over the top of the current page, things will be done slightly differently, typically using the NavigationPage's Popped event).

    I do have access to the Xamarin Profiler, but for some reason it is not working at the moment, so I'm guessing this will be the first thing I need to solve. How are you checking for memoryleaks in your "poor-person" equivalent for this?

    I subclass classes such as Command, and add wrappers around event handlers, with (when conditionally-compiled) the constructors and finalisers updating a collection of created-but-unfinalised objects, with that collection holding class names and counts, not references to the objects themselves. It slows things down when compiled-in, but it's an easy and effective way to spot leaks early (few people run the profiler regularly during development).

    (I might update the collection at some point to include peak counts as well, but haven't done so yet)

    I managed to get the Xamarin Profiler to work (update of VS) and it seems that even just simple navigation forward/backward into my PageA from the menu in causing the number of object counts to rise. I will try to unsubscribe from all events in my OnDisappearing in order to see if this will make any difference. I am wondering if it is possible to see what is preventing a page from being collected by the GC?

  • LapsusLapsus USMember ✭✭

    @RHudson said:
    What if you change the message signature after the user logs in?

    instead of

    MessagingCenter.Send<MainPage, string> (this, "Hi", "John");
    

    As a test try this, do the pages still get confused?

    var guid = Guid.NewGuid();
    MessagingCenter.Send<MainPage, string> (this, guid, "John");
    

    Show your code, how are you sub/unsubscribing?

    Also... is it an option for you to use plain old C# events instead of MessagingCenter?

    The unsubscribing is working, since if I navigate from PageA with a simple Navigation.PopAsync, the message will no longer hit the page, even though it is still living in memory.

    //This how I subscribe in the OnAppearing of PageA
    MessagingCenter.Unsubscribe<string>(this, TopicEnum.BARCODE.ToString());
    MessagingCenter.Subscribe<string>(this, TopicEnum.BARCODE.ToString(), async (arg) => {
        await ParseScanResult(arg, true);
    });
    
    //And this is how I unsubscribe in my OnDisappearing of PageA
    MessagingCenter.Unsubscribe<string>(this, TopicEnum.BARCODE.ToString());
    
  • LapsusLapsus USMember ✭✭
    edited March 29 Accepted Answer

    @Lapsus said:

    @RHudson said:
    What if you change the message signature after the user logs in?

    instead of

    MessagingCenter.Send<MainPage, string> (this, "Hi", "John");
    

    As a test try this, do the pages still get confused?

    var guid = Guid.NewGuid();
    MessagingCenter.Send<MainPage, string> (this, guid, "John");
    

    Show your code, how are you sub/unsubscribing?

    Also... is it an option for you to use plain old C# events instead of MessagingCenter?

    The unsubscribing is working, since if I navigate from PageA with a simple Navigation.PopAsync, the message will no longer hit the page, even though it is still living in memory.

    //This how I subscribe in the OnAppearing of PageA
    MessagingCenter.Unsubscribe<string>(this, TopicEnum.BARCODE.ToString());
    MessagingCenter.Subscribe<string>(this, TopicEnum.BARCODE.ToString(), async (arg) => {
        await ParseScanResult(arg, true);
    });
    
    //And this is how I unsubscribe in my OnDisappearing of PageA
    MessagingCenter.Unsubscribe<string>(this, TopicEnum.BARCODE.ToString());
    

    I think I finally figured out this, it turns out the error was caused by my error of assuming that Xamarin.Forms is working somewhat differently as it apparently is. The leak was indeed caused by the remaining subscriptions to the MessagingCenter from the "disposed"pages. But how did those subscriptions came to life, if I subscribe in the OnAppearing and unsubscribe in the OnDisappearing? Well my code is structured like this:

    protected override void OnAppearing()
    {
        base.OnAppearing();
        //Check user session
        //Subscribe to MessagingCenter
    }
    
    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        //Unsubscribe from MessagingCenter
    }
    

    And in my "Check user session" I do the replacing of the MainPage, if the session is marked as expired. Now my wrong assumption comes into play: I assumed that in that case, the OnDisappearing will fire after OnAppearing has finished it's execution since probably pages live in some kind of a state machine. Well what happens is not like I assumed, but instead the replacing of the MainPage causes the OnDisappearing to fire instantly and only when OnDisappearing finishes, the OnAppearing execution will resume. This of course caused the subscribe calls to be called AFTER the unsubscribe calls thus resulting in the live subscriptions, that prevented the pages from being collected by the GC and to remain listening to the messaging center messages. I solved this so now I check the status of the CheckUserSession call, and if session has expired, I break the execution of the OnAppearing from that point.

    I think I will really need to create a wrapper for the MessagingCenter where I will keep the track of all active subscriptions, since this is the second time in my XF journey that the MessagingCenter got the better part of me :)

    Thank you both for your help, I don't think I would figure this out in any reasonable time, without digging around in the Xamarin Profiler, and double checking the MessagingCenter subscriptions and removal of them.

  • JohnHardmanJohnHardman GBUniversity mod

    @Lapsus

    Glad it's working. Don't forget to Like and/or mark as Accepted Answer any posts that helped you.

Sign In or Register to comment.