26

In an existing Android project I've encountered the following piece of code (where I inserted the debugging litter)

ImageView img = null;

public void onCreate(...) {

    img = (ImageView)findViewById(R.id.image);

    new Thread() {
        public void run() {
            final Bitmap bmp = BitmapFactory.decodeFile("/sdcard/someImage.jpg");
            System.out.println("bitmap: "+bmp.toString()+" img: "+img.toString());
            if ( !img.post(new Runnable() {
                public void run() {
                    System.out.println("setting bitmap...");
                    img.setImageBitmap(bmp);
                    System.out.println("bitmap set.");
                }
            }) ) System.out.println("Runnable won't run!");
            System.out.println("runnable posted");
        }
    }.start();

New to Android development, and having googled around, I understand that this is the way to do stuff without blocking the main (UI) thread, while still setting the image on the UI thread after decoding. (at least according to android-developers) (which I have verified by logging Thread.currentThread().getName() at various places)

Now sometimes the image just doesn't show up, and stdout only says

I/System.out( 8066): bitmap: android.graphics.Bitmap@432f3ee8 img: android.widget.ImageView@4339d698
I/System.out( 8066): runnable posted

with not a trace of the messages from the Runnable. So appearantly the Runnable doesn't run(), although img.post() returns true. Pulling the ImageView in onCreate() and declaring it final doesn't help.

I'm clueless. Simply setting the bitmap directly, while blocking the UI thread, does fix things, but I want to get things right. Does anybody understand what's going on here?

(ps. this was all observed on an Android 1.6 phone and android-3 sdk)

mvds
  • 45,755
  • 8
  • 102
  • 111
  • similar questions with useful answers: [Whats the difference between Activity.runOnUiThread(runnable action) and Handler.post()?](http://stackoverflow.com/questions/1839625/whats-the-difference-between-activity-runonuithreadrunnable-action-and-handler), [Difference between Handler.post(Runnable r) and Activity.runOnUiThread(Runnable r)](http://stackoverflow.com/questions/7452884/difference-between-handler-postrunnable-r-and-activity-runonuithreadrunnable) – Richard Le Mesurier Jun 27 '12 at 14:37

6 Answers6

59

If you look at the docs for View.post there's some relevant info:

This method can be invoked from outside of the UI thread only when this View is attached to a window.

Since you're doing this in onCreate, it is likely that sometimes your View will not be attached to the window yet. You can verify this by overriding onAttachedToWindow and putting something in the logs and also logging when you post. You'll see that when the post fails, the post call happens before onAttachedToWindow.

As the others have mentioned, you can use Activity.runOnUiThread or provide your own handler. However, if you want to do it directly from the View itself, you can simply get the View's handler:

view.getHandler().post(...);

This is especially useful if you have a custom view that includes some sort of background loading. There's also the added bonus of not having to create a new separate handler.

kabuko
  • 36,028
  • 10
  • 80
  • 93
  • 12
    I've only investigated this briefly, but it appears that when the view is not attached to a window it also doesn't have a handler. – ThomasW Aug 10 '12 at 09:34
  • @ThomasW My solution worked in my scenario but YMMV for sure. As far as I know, the docs don't specify when `getHandler()` is going to be valid, but on examination of the [code](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3_r1/android/view/View.java#View.dispatchAttachedToWindow%28android.view.View.AttachInfo%2Cint%29) it looks like my solution only works when the view is at least *about* to be attached. If you never even try to attach it to a window then you're correct, there will be no handler available. – kabuko Aug 10 '12 at 17:19
  • 1
    @kabuko this should be edited now that the functionality has changed. The docs no longer have the clause you reference. I would assume this is because, according to the source, the functionality seems to have changed: `// Execute enqueued actions on every traversal in case a detached view enqueued an action \n getRunQueue().executeActions(attachInfo.mHandler);` This means that posting to a View should work provided the View has been attached once (where it gets the initial reference to the ViewRootImpl's Handler). But, you cannot still post to a view before it's been initially attached. – dcow Oct 15 '13 at 21:40
  • @kabuko View.post() now also returns a boolean to signal whether the action was successful. I think it would now be advisable to check that value to determine whether the post was successful (since *any* successfully posted runnable should now be executed regardless of whether the View is attached or not). – dcow Oct 15 '13 at 21:42
  • @DavidCowden Thanks for pointing this out. I haven't had the time to take a close look to see what the current behavior is exactly, but it definitely seems that the documentation has changed. The boolean return value was always there. I didn't point it out because it simply wasn't useful in this situation (it was successfully posted, but not executed). Hopefully at some point I'll take some time and figure out exactly what happens now and update this answer for newer versions of Android. For the record, I *think* this functionality changed around 4.3, though it'd be good to find out for sure. – kabuko Oct 16 '13 at 18:34
  • @kabuko yeah it was definitely in the last 2 API levels at least. – dcow Oct 16 '13 at 20:09
  • 1
    FTR: This seems to still be an issue with 4.4: There are situations where the handler is null, post() returns true and the Runnable is never invoked... – Stefan Haustein Jun 30 '14 at 23:26
  • @StefanHaustein: +1. On 4.1 and 4.2 too. Using `post` in a thread created in a fragment onCreateView, the `post` returns `true`, but the `run` may or may not be called, without any notification – njzk2 Sep 29 '14 at 19:24
  • 1
    I can not see this comment `This method can be invoked from outside of the UI thread only when this View is attached to a window.` any more in android 5.0 doc, but It seems this problem is still there – Charlesjean Dec 24 '14 at 10:21
  • @StefanHaustein +1 And I also found out that at least on 4.4 calling view.getHanlder().post() outside an ui thread just hangs. I tried to put some logging after this call - never got executed... – dimsuz Jul 15 '15 at 11:00
  • Actiually, this has never been true, but at some point the JavaDoc for View.java did wrongly state that "View.post only works from another thread when the View is attached to a window". This was [fixed](https://github.com/android/platform_frameworks_base/commit/edc900528937cd03f0d3a94fdf73d019324a2054) on Oct. 15, 2012, but took a while to penetrate the minds of Android developers. The caveat probably is that if the view is not attached to a window in the future, the posted Runnable will never be executed. – Alex Cohn Jun 26 '16 at 12:00
  • better is : new Handler( Looper.getMainLooper() ).post( Runnable ); – Mustafa R.Mahdi Jul 27 '17 at 15:52
  • `View.post` and `postDelayed` are so unpredictable! They work flawlessly in some Android devices and fail miserably in some. Duh! – sud007 Sep 07 '17 at 10:05
  • Runnable from View.post in onCreate is stored in a RunQueue rather than add it to message queue directly. This Runnable will execute after next performTraversals. Before Android 7.0, Runnable will not execute if you View.post it in a thread in onCreate. Yes, this issue was fixed in 7.0. – Bos Dec 26 '17 at 08:05
10

I extended ImageView class to solve this problem. I collect runnables passed to post while view not attached to window and in onAttachedToWindow post collected runnable.

public class ImageView extends android.widget.ImageView
{
    List<Runnable> postQueue = new ArrayList<Runnable>();
    boolean attached;

    public ImageView(Context context)
    {
        super(context);
    }

    public ImageView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    public ImageView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onAttachedToWindow()
    {
        super.onAttachedToWindow();

        attached = true;

        for (Iterator<Runnable> posts = postQueue.iterator(); posts.hasNext();)
        {
            super.post(posts.next());
            posts.remove();
        }
    }

    @Override
    protected void onDetachedFromWindow()
    {
        attached = false;
        super.onDetachedFromWindow();
    }

    @Override
    public boolean post(Runnable action)
    {
        if (attached) return super.post(action);
        else postQueue.add(action);
        return true;
    }
}
Bugs Happen
  • 2,169
  • 4
  • 33
  • 59
Nik
  • 7,114
  • 8
  • 51
  • 75
10

I think the problem is you are updating the UI (ImageView) with a separate thread, which is not the UI Thread. The UI can only be updated by the UI Thread.

You can solve this by using Handler:

Handler uiHandler;

public void onCreate(){
    ...
    uiHandler = new Handler(); // This makes the handler attached to UI Thread
    ...
}

Then replace your:

if ( !img.post(new Runnable() {

with

uiHandler.post(new Runnable() {

to make sure the imageview is updated in the UI Thread.

Handler is a quite confusing concept, I also took hours of research to really understand about this ;)

xandy
  • 27,357
  • 8
  • 59
  • 64
  • I don't understand why, but this seems to work as well! So anything is more reliable than `View.post()`. Thanks! – mvds Nov 03 '10 at 11:29
  • Surprising yet again! This works. Thanks! But I wonder `View.post` and `postDelayed` are so unpredictable! They work flawlessly in some Android devices and fail miserably in some. Duh! – sud007 Sep 07 '17 at 10:05
6

I don't see anything obviously wrong with what you have there; calling View.post() should cause it to run on the UI thread. If your Activity went away (perhaps through a screen rotation), then your ImageView wouldn't be updated, but I would still expect a log entry to say "setting bitmap ...", even if you couldn't see it.

I suggest trying the following and see if it makes a difference:

1) Use Log.d (the standard Android logger) rather that System.out

2) Pass your Runnable to Activity.runOnUiThread() rather than View.post()

Shawn Lauzon
  • 6,234
  • 4
  • 35
  • 46
  • I don't understand why, but this seems to work! In one out of ten tries, I got a segmentation fault (apparently in libc), but I can't believe this is related. – mvds Nov 03 '10 at 11:27
  • Good call @Shawn. I never noticed runOnUiThread() function. Interesting enough, I checked the source code and runOnUiThread is using Handler to implement. So @mvds, both works is not coincidence, they are using same method under the hood. – xandy Nov 03 '10 at 15:20
  • What's still not clear to me is why @mdvs's solution doesn't work. The Google article at http://developer.android.com/resources/articles/painless-threading.html references View.post(Runnable), which is what he used, and the Javadoc says "Causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread." – Shawn Lauzon Nov 04 '10 at 00:58
  • 1
    I have the exactly same problem as mvds. And yeah, it also works like one out of 3 - but when I added some more Log.i(...) - it works better, like 1 out of 7 , and I took out thoses Log.i() , it works again in 1 out of 3 .... So I just don't understand why - but it seems that when the code get "slower" just a little bit then the View.post() works ! Is that a bug in SDK ? – hungson175 Apr 01 '12 at 22:13
  • 1
    Updated: I just pass in a Handler, and use handler.post() - it works flawlessly ! And one more thing: be careful when pass Activity to the thread - it could be a memory-leak (in my case, I use the thread to load images from web, and the thread should be Singleton - static & pointer to Activity: dangerous as in http://developer.android.com/resources/articles/avoiding-memory-leaks.html – hungson175 Apr 01 '12 at 22:24
3

Use the following code, can post your code to MainThread anytime anywhere, but not depends any Context or Activity. That can prevent view.getHandler() failure or tedious onAttachedToWindow() stuffs etc.

    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            //TODO
        }
    });
fantouch
  • 1,159
  • 14
  • 12
1

I had the same problem, and using view.getHandler() also failed because the handler was not present. runOnUiThread() solved the problem. Presumably this does actually do some queueing until the UI is ready.

The cause for me was calling the icon load task in a base class and the result being returned so quickly that the main class hadnt estabished the view (getView() in fragment).

I'm a bit suspicious that it might spuriously fail sometime. But I'm now ready for it! Thanks guys.

Ratatat Richie
  • 131
  • 1
  • 10