Handler changing UI causes CalledFromWrongThreadEx

2019-07-18 19:51发布

问题:

I've created a Handler that can be accessed from anywhere within the activity and also written a method to make it easier to call the handler:

private Handler textFromBGThread = new Handler() {
    @Override
    public void handleMessage (Message msg) {
        // Get the string from the msg
        String outputString = msg.getData().getString("Output");
        // Find the TextView
        TextView Output = (TextView)findViewById(R.id.ConsoleOutputView);
        // Display the output
        Log.i("TextOutput","About to display message: " + outputString);
        Output.setText(Output.getText() + outputString);
        Log.i("TextOutput","Message displayed");
    }
};

private void TextOutputWrapper (String outputText) {
    Message msg = new Message();
    Bundle bndle = new Bundle();
    bndle.putString("Output", "\n" + outputText);
    msg.setData(bndle);
    textFromBGThread.handleMessage(msg);
}

So then this can be called from a background thread simply with:

TextOutputWrapper("Attemping to connect...");

This will work 1+ times, however, the actual visual change will cause a CalledFromWrongThreadException to be thrown. Being new to Java & Android, I'm stuck on why this is happening.

I have noticed that the crash tends to happen when there's a slightly longer time period between calls & that if the calls to TextOutputWrapper(String) are happening very quickly after one another, then it works. For example, this:

int i = 0;
while (i < 200) {
    TextOutputWrapper(String.valueOf(i));
    i++;
}

works fine.

Having looked at LogCat, it seems that the garbage collector frees up some resources and then the next time TextOutputWrapper(String) is called, it crashes (when Output.SetText(String) is called, to be precise), although I'm not exactly sure why that would cause this error.

回答1:

There's a few things I'd change here:

Using Handler

A Handler is useful if you want to trigger the UI to update, and do so from a non-UI thread (aka "background" thread).

In your case, it's not serving that purpose. You are directly calling

textFromBGThread.handleMessage(msg);

It's not designed for you to do that. The way you are supposed to use Handler is to implement what you want done to the UI in the handleMessage(Message) method. You did that. But, you shouldn't directly call handleMessage(). If you do that, then handleMessage() will be called from whatever thread invokes TextOutputWrapper(). If that's a background thread, then that's wrong.

What you want to do is to call the handler's sendMessage(Message) method (or one of the other available variants). sendMessage() will put your message in a thread-safe queue, that is then processed on the main thread. The main thread will then invoke your handler's handleMessage(), passing it back the queued message, and allowing it to safely change the UI. So, change TextOutputWrapper() to use this:

private void TextOutputWrapper (String outputText) {
    Message msg = new Message();
    Bundle bndle = new Bundle();
    bndle.putString("Output", "\n" + outputText);
    msg.setData(bndle);
    textFromBGThread.sendMessage(msg);
}

Java Conventions

This code is a bit hard to read, for an experienced Java developer. In Java, typical coding standards reserve upper case names for things like classes, while methods start with lower case letters. So, please rename the method to:

private void textOutputWrapper (String outputText);

or, better yet, since this is in fact a method, and not a wrapper, per se, rename to something like

private void outputText(String text);

Safe Threading Alternatives

Finally, I might recommend that if you simply want a method that allows you to safely modify the UI from any thread, use another technique. I don't find Handler to be that easy to use for beginners.

private void outputText(final String outputString) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            // Find the TextView
            TextView output = (TextView)findViewById(R.id.ConsoleOutputView);
            // Display the output
            Log.i("TextOutput","About to display message: " + outputString);
            output.setText(Output.getText() + outputString);
            Log.i("TextOutput","Message displayed");
        }
    });
}

runOnUiThread() is a method available in every Activity.

I'll also point you to some general docs on understanding threading in Android:

http://www.vogella.com/articles/AndroidBackgroundProcessing/article.html

http://android-developers.blogspot.com/2009/05/painless-threading.html