Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Could a BeginInvokeOnMainThread method be looping and causing a memory leak?

I have an application that works but after a while when I debug on my iPhone it hangs the phone and the only way I can recover is a hard reset of the button on the side and the home button.

First of all, could that be because my application has a memory leak?

Here's the code for the application. In particular, I am looking at the BeginInvokeOnMainThread method. Can someone tell me if they can see if there could be any problems with the way that it is implemented? Also, what's the purpose of the .ContinueWith((arg).

namespace Japanese
{
    public partial class PhrasesFrame : Frame
    {

        CancellationTokenSource cts = new CancellationTokenSource();

        public PhrasesFrame(PhrasesPage phrasesPage)
        {
            InitializeComponent();
            this.phrasesPage = phrasesPage;
            AS.phrasesFrame = this;
            Device.BeginInvokeOnMainThread(() => ShowCards(cts.Token).ContinueWith((arg) => { }));
        }

        public void Disappearing()
        {
            cts.Cancel();
        }

        public async Task ShowCards(CancellationToken ct)
        {
            AS.cardCountForSelectedCategories = App.DB.GetCardCountForSelectedCategories();
            while (!ct.IsCancellationRequested)
            {

                await Task.Delay(500);
            }
        }
    }
}
like image 980
Alan2 Avatar asked Aug 06 '17 09:08

Alan2


2 Answers

ContinueWith

First, let's address your question about .ContinueWith((arg) => { })). ContinueWith tells more code to execute once the original Task has completed. In our case, the code inside of ContinueWith will run once Device.BeginInvokeOnMainThread(() => ShowCards(cts.Token) has finished.

In this case, there is no code inside of ContinueWith, so we can remove it.

Freezing

Yes, I can see that this code has potential to freeze the UI.

BeginInvokeOnMainThread will queue an Action to run on the Main Thread (also known as the UI Thread). The Main Thread is constantly listening for user input (tapping a button on the screen, pinch-to-zoom, etc.), and if this thread is busy doing a long-running task, it will not be able to respond to a user's input until it has finished; thus your app will appear frozen.

The code await Task.Delay(500); is being called by the Main Thread. We are thus telling the Main Thread to freeze itself for 500 milliseconds, and looping that indefinitely.

One solution would be to wrap this code in Task.Run which would put it in a background-thread and free the Main Thread to listen/respond to user input.

Task.Run(async () => 
{ 
    while (!ct.IsCancellationRequested)
    {
          await Task.Delay(500);
    }
}

More Threading Recommendations

  • Only use BeginInvokeOnMainThread when you need to update the UI. 99% of code can run on a background thread with no problems. The 1%, however, is code that updates the UI; any code that updates the UI must be run on the Main Thread.

  • If a task that takes longer than the refresh rate of the screen to execute, perform it on a background thread. For example, if the screen's refresh rate is 60Hz, it is updating 60-times per second, every 16.7ms. So if we have a block of code that takes 20ms to execute, we need to execute it on a background thread to ensure that we don't freeze the app and drop any frames.

    • The code above looks like it is accessing a database, which I would highly recommend moving to a background thread like so
await Task.Run(() => AS.cardCountForSelectedCategories = App.DB.GetCardCountForSelectedCategories());
like image 168
Brandon Minnick Avatar answered Oct 18 '22 03:10

Brandon Minnick


First, if you are concerned about a memory leak, you can check for low-memory warnings in the device logs (accessible through XCode), or override the ReceiveMemoryWarning method in your app delegate to log an error.

Secondly, there's nothing obviously wrong with the way you're calling BeginInvokeOnMainThread that would cause a leak. The ContinueWith is a no-op that doesn't affect the operation of the code - I'm guessing it's there to avoid a compiler warning that you're not awaiting the task.

Thirdly, if you suspect that this code is causing a leak, you should use logging and/or breakpoints to confirm that it's behaving as expected. Is the task correctly cancelled when you navigate away from the page? Do you see multiple instances of of the ShowCards task running? If this code turns out to be behaving correctly, then the source of the hang lies elsewhere in your app. For instance, it looks like you're making a database call twice a second - maybe it's not cleaning up resources properly.

like image 2
David Oliver Avatar answered Oct 18 '22 02:10

David Oliver