简体   繁体   中英

How to safely write to GUI from different thread in Xamarin.Android

I've downloaded a trial version of Xamarin and am currently playing around with it (in Visual Studio 2010).

One of the tests that I wanted to do was to see how to make an Activity where I update a control on the GUI via a BackgroundWorker thread - Specifically, I was interested to see how different the Mono syntax would be compared to a regular Windows Forms (C#) syntax.

To test this out, I created an Android Application (again, in VS2010) targeting API Level 17 (Android 4.2). The general functionality of the app will change the text value of an EditText control from within a _DoWork() BackgroundWorker event handler.

Here's the code..

//Xamarin.Android app
[Activity(Label = "Cross-thread Test", MainLauncher = true)]
public class Activity1 : Activity
{
    EditText labelDisplay;
    BackgroundWorker bgWorker;
    int counter = 0;

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.Main);

        this.bgWorker = new BackgroundWorker();
        this.bgWorker.WorkerSupportsCancellation = true;
        this.bgWorker.DoWork += new DoWorkEventHandler(bgWorker_DoWork);

        Button buttonStart = FindViewById<Button>(Resource.Id.buttonStart);
        buttonStart.Click += new EventHandler(buttonStart_Click);

        Button buttonStop = FindViewById<Button>(Resource.Id.buttonStop);
        buttonStop.Click += new EventHandler(buttonStop_Click);

        labelDisplay = FindViewById<EditText>(Resource.Id.labelDisplay);
        labelDisplay.Text = "Click Start";
    }

    void bgWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        while (true)
        {
            if (this.bgWorker.CancellationPending)
            {
                RunOnUiThread(() => labelDisplay.Text = "Click Start");
                break;
            }
            else
            {
                counter++;

                            // This causes GREF to increase to 2001
                RunOnUiThread(() => labelDisplay.Text = counter.ToString());
            }
        }
    }

    private void buttonStart_Click(object sender, EventArgs e)
    {
        if (this.bgWorker != null && !this.bgWorker.IsBusy)
            this.bgWorker.RunWorkerAsync();
    }

    private void buttonStop_Click(object sender, EventArgs e)
    {
        if (this.bgWorker != null && this.bgWorker.IsBusy)
            this.bgWorker.CancelAsync();
    }
}

For whatever it's worth, I'm attempting to use the RunOnUiThread() in the similar way that I use Invoke() in a regular Windows Forms like this...

//Regular Windows Forms app
void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
    while (true)
    {
        if (this.bgWorker.CancellationPending)
            break;
        else
        {
            counter++;
            //This isn't possible in a Xamarin.Android app, which is why I'm using RunOnUiThread() instead
            this.Invoke((Action)(() => { this.labelDisplay.Text = counter.ToString(); })); 
        }
    }
}

The Xamarin.Android application will crash where the Debug output reveals the following error (I've only included notable info)...

09-28 18:09:39.231 D/dalvikvm(  731): GREF has increased to 1701
09-28 18:09:39.461 D/dalvikvm(  731): GREF has increased to 1801
09-28 18:09:40.192 D/dalvikvm(  731): GREF has increased to 1901
09-28 18:09:40.271 D/dalvikvm(  731): GC_CONCURRENT freed 305K, 7% free 6175K/6599K, paused 3ms+5ms
09-28 18:09:40.531 D/dalvikvm(  731): GREF has increased to 2001
09-28 18:09:40.531 W/dalvikvm(  731): JNI global reference table (0x475fd0) dump:
09-28 18:09:40.531 W/dalvikvm(  731):   Last 10 entries (of 2001):
09-28 18:09:40.531 W/dalvikvm(  731):      2000: 0x40fa4e90 java.lang.NoClassDefFoundError
09-28 18:09:40.541 W/dalvikvm(  731):      1999: 0x40fb2e78 mono.java.lang.RunnableImplementor
.
.
.
09-28 18:09:40.581 E/dalvikvm(  731): Excessive JNI global references (2001) //OOPS!
09-28 18:09:40.581 E/dalvikvm(  731): VM aborting
09-28 18:09:40.581 E/mono-rt (  731): Stacktrace:
09-28 18:09:40.581 E/mono-rt (  731): 
09-28 18:09:40.592 E/mono-rt (  731):   at <unknown> <0xffffffff>
09-28 18:09:40.592 E/mono-rt (  731):   at (wrapper managed-to-native) object.wrapper_native_0x408027e9 (intptr,intptr) <IL 0x00026, 0xffffffff>
09-28 18:09:40.592 E/mono-rt (  731):   at Android.Runtime.JNIEnv.NewGlobalRef (intptr) [0x00000] in /Users/builder/data/lanes/monodroid-mlion-monodroid-4.8.2-branch/bdc709d1/source/monodroid/src/Mono.Android/src/Runtime/JNIEnv.cs:389

I read Xamarin's Troubleshooting page that states

Dalvik's JNI layer only supports a limited number of JNI object references to be valid at any given point in time. When this limit is exceeded, things break.

The GREF (global reference) limit is 2000 references in the emulator, and ~52000 references on hardware.

You know you're starting to create too many GREFs when you see messages such as this in the Android Debug Log:

You'll notice in the error log that my code increased the GREF to 2001.

Based on the troubleshooting notes above, I'm assuming that the RunOnUiThread() is creating a JNI object for each iteration of the while loop. If this is the case, why is this occurring and what should I do to write to the GUI from a different thread safely?

RunOnUiThread queues the work.

The technique for updating the UI is OK, it's just that with no pause in the loop, that queue hits 2000 items almost instantaneously.

I bet it would be fine with a short Thread.Sleep() in there...

try to write your UI update in this

Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
{
     // code to execute
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM