3

In the Android framework, if a TextView's setText() method is called, and after it returns Thead.sleep() is called, then the screen of the device does not display the given text until after sleep() has returned. Why?

public class SleeperActivity extends Activity {
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.main);
    }

    public void doDelay(View view) {
        try {
            TextView textView = (TextView) view;
            textView.setText("Sleeping");
            Log.d("Sleeper", "This log entry is invoked before sleeping");
            Thread.sleep(5000L);
            textView.setText("Ready");
        } catch (InterruptedException e) { finish(); }
    }
}

The layout, not shown, has a button whose onClick attribute is set to doDelay().

When the above code is compiled and run, and the button is clicked, the text passed in the first invocation of setText() does not appear on the screen until after the thread has slept for five seconds, even though the log entry appears in the log before those five seconds begin to elapse.

Why does this happen, and what ought I to do about it to make the text passed in the first call to setText() appear on the screen before the thread begins to sleep?

Adam Mackler
  • 1,980
  • 1
  • 18
  • 32

2 Answers2

3

1)Never sleep that long on the UI thread. In fact, never sleep on it at all. That causes it to block and makes the phone unresponsive

2)setText calls invalidate on the view. That will cause the framework to draw the view again as soon as its able- but that means needs to finish the current things its doing and get back to the main Looper (basically, it needs to return from whatever method in your code it first calls). Until then, the change won't be visible on screen.

Edit: to put it another way, inside the framework we have

do{
    Message msg = nextMessage();
    if(msg.what == ON_CREATE){
       currentActivity.onCreate();
    }
    else if(msg.what == DRAW){
        rootView.onDraw();
        //draw entire screen
    }
    else 
      ...  //Thousands of more events like touches, handlers, etc

It can't get to the draw message until its done with the current message. Its single threaded.

Gabe Sechan
  • 90,003
  • 9
  • 87
  • 127
  • Regarding #1: I know, thank you. This is just a pedagogical example that I'm using to understand this phenomenon. This is not production code. Regarding #2: I don't understand you. I think you left out some words. What does "current things its doing" mean? What does "code it first calls mean?" Obviously `setText()` is a "method in my code it first calls" before `sleep()` is invoked, and that method returns before `sleep()` is called. Clearly I'm misunderstanding you. Could you show me the minimal necessary change to the example code I gave that would cause the text to appear as desired? – Adam Mackler May 16 '13 at 19:46
  • In the framework, there's basically a giant message loop. That loop waits for events like messages in Handlers, IO events, events generated by the Android framework like onCreate, etc. When you call setText(), it sends a DRAW message to that loop. The view will not actually be drawn until the loop can process that DRAW command. That won't happen until after the onCreate function finishes. There is no way to make it happen faster than that. – Gabe Sechan May 16 '13 at 19:48
  • What is it about `onCreate()` that prevents the loop from processing the DRAW command until it's finished? I know it doesn't take five seconds to display the text if the `sleep()` isn't in there, so there must be a way to (1) display the text, (2) wait for the loop to process DRAW, and (3) invoke `sleep()` in that order. Is the loop's processing of the DRAW command dependent on (a) the passage of time, or (b) the `onCreate()` method finishing? It sounds you you're saying it's (b), but you also use the word "faster" which makes it sound like it's a time issue. What's the workaround? – Adam Mackler May 16 '13 at 20:03
  • The loop executes on the same thread as onCreate. onCreate is actually called by the loop. So the message loop won't ask for the next message until it gets back to the giant while statement at the top. Which it won't be able to do until its done with the onCreate. Maybe my edit will make it clearer. – Gabe Sechan May 16 '13 at 20:04
  • Okay, I'm clear on what doesn't work, but I still want to know what does work. If someone hired you to write an Android app that when launched: first displays some text on the screen, then as soon as that text is visible blocks the main thread for five seconds, then after five seconds displays some other text on the screen, is this possible or not? If it's possible, then how does the code you deliver to do it differ from the code I posted? – Adam Mackler May 16 '13 at 20:15
  • Displaying some other text after 5s is easy enough- send a message to a handler with a 5s delay, and set the new text in the handler. I wouldn't block the main thread at all. If you absolutely need to not accept other IO during those 5 seconds, you need to ignore them in the appropriate functions based on a flag (which would get turned back on when the handler fires). – Gabe Sechan May 16 '13 at 20:21
  • I just saw your edit, thank you. Can you tell me what file that snippet is from? – Adam Mackler May 16 '13 at 21:03
2

The reason for the observed behavior is that Views are only redrawn after the completion of each cycle through the event loop. The android onCreate() method, as well as touch events such as clicking a button, are invoked by the framework from within the event loop. Thus, methods such as onCreate() or onClick() will execute to completion before any Views are redrawn, and there will be no visible effect on the display until after those methods have returned. Changes to Views made from within those methods will only become visible after completion of the event loop cycle from within which the framework invoked said method (onCreate(), onClick(), etc.).

To achieve the behavior requested in the question, invocation of the sleep() method must occur after completion of the event loop cycle in which you invoked setText() with the text you want to be displayed during time the thread is blocking. One way to do this is simply to replace the call to sleep() with a call to View.post(). View.post() takes a Runnable whose run() method will be called after the current cycle of the event loop has completed and the framework has displayed the text you want to see during the time the thread is blocking. Change the doDelay() method in the question by placing the call to Thread.sleep() inside a Runnable's run() method like this:

    public void doDelay(View view) {
        final TextView textView = (TextView) view;
        textView.setText("Sleeping");
        textView.post(new Runnable() {
            public void run() {
                try {
                    Log.d("Sleeper", "Thread " + Thread.currentThread() + " going to sleep...");
                    Thread.sleep(5000L);
                    Log.d("Sleeper", "Thread " + Thread.currentThread() + " now awake");
                    textView.setText("Ready");
                } catch (InterruptedException e) { finish(); }
            }
        });
    }

My thanks goes to to Gabe Sechan, whose guidance got me unstuck enough to answer my own question.

Community
  • 1
  • 1
Adam Mackler
  • 1,980
  • 1
  • 18
  • 32