可运行对象已成功发布但未运行。

26
在一个现有的Android项目中,我遇到了下面这段代码(其中我插入了调试信息):
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();

我刚开始学习Android开发,通过Google搜索得知,这是在不阻塞主线程的情况下完成任务的方法,同时在解码后仍然可以在UI线程上设置图像。根据android-developers的说法(我已通过在各个地方记录Thread.currentThread().getName()进行了验证)。

但是,有时候图像就是无法显示,标准输出只会说:

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

没有任何来自Runnable的消息痕迹。因此,显然Runnable没有运行(run()),尽管img.post()返回true。在onCreate()中拉动ImageView并声明它为final也无济于事。
我很困惑。直接设置位图可以解决问题,但会阻塞UI线程,我想把事情做对。有人能理解这里发生了什么吗?
(附:这全部是在Android 1.6手机和android-3 sdk上观察到的)

6个回答

59
如果您查看View.post的文档,会发现一些相关信息:
此方法只能在将此View附加到窗口时从UI线程外部调用。
由于您在onCreate中执行此操作,所以有可能您的View尚未附加到窗口。您可以通过覆盖onAttachedToWindow并在日志中记录某些信息,然后记录发布时间来验证此内容。您会发现当发布失败时,发布调用会在onAttachedToWindow之前发生。
正如其他人提到的,您可以使用Activity.runOnUiThread或提供自己的处理程序。但是,如果您想直接从View自身执行此操作,则可以简单地获取View的处理程序:
view.getHandler().post(...);

如果您有一个包含某种背景加载的自定义视图,那么这特别有用。此外,不必创建新的单独处理程序也是额外的好处。


12
我只是简单调查了一下,似乎当视图没有附着在窗口上时,它也没有处理程序。 - ThomasW
1
@kabuko,现在功能已经改变,应该对此进行编辑。文档中不再包含你提到的条款。根据源代码,我认为这是因为功能似乎已经改变了:// Execute enqueued actions on every traversal in case a detached view enqueued an action \n getRunQueue().executeActions(attachInfo.mHandler); 这意味着,只要View已经附加过一次(在那里它获得了对ViewRootImpl的Handler的初始引用),则可以将其发布到View上。但是,在View被初始附加之前仍然不能将其发布到View上。 - dcow
@kabuko View.post() 现在也返回一个布尔值来表示操作是否成功。我认为现在最好检查该值以确定帖子是否成功(因为任何成功发布的可运行项现在都应该被执行,无论视图是否附加)。 - dcow
1
FTR:这似乎仍然是4.4版的问题:存在处理程序为null的情况,post()返回true并且Runnable从未被调用... - Stefan Haustein
1
在Android 5.0文档中,我再也找不到这个注释“只有在此视图附加到窗口时,才能从UI线程外部调用此方法。”然而,看起来这个问题仍然存在。 - Charlesjean
显示剩余9条评论

10

我认为问题在于您正在使用单独的线程更新UI(ImageView),这不是UI线程。UI只能被UI线程更新。

您可以通过使用Handler来解决此问题:

Handler uiHandler;

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

然后替换您的:
if ( !img.post(new Runnable() {

使用

uiHandler.post(new Runnable() {

为了确保ImageView在UI线程上更新,

Handler是一个相当令人困惑的概念,我也花了几个小时的时间进行研究才真正理解它 ;)


我不明白为什么,但这似乎也可以工作!因此,任何东西都比View.post()更可靠。谢谢! - mvds
又惊又喜!这个方法可行。谢谢!但我想知道View.postpostDelayed为什么如此不可预测!它们在一些安卓设备上运行得非常完美,而在一些设备上则失败了。唉! - sud007

10

我扩展了ImageView类来解决这个问题。当视图未附加到窗口时,我会收集传递给post的可运行对象,并在onAttachedToWindow中发布已收集的可运行对象。

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;
    }
}

6
我看不出你的代码有什么明显的错误;调用View.post()应该会使其在UI线程上运行。如果你的Activity消失了(可能是因为屏幕旋转),那么你的ImageView就不会被更新,但我仍然希望看到一个日志条目说“设置位图...”,即使你看不到它。
我建议尝试以下方法,看看是否有所改善:
1)使用Log.d(标准的Android日志记录器)而不是System.out
2)将你的Runnable传递给Activity.runOnUiThread()而不是View.post()

我不明白为什么,但这似乎有效!在十次尝试中的一次中,我遇到了分段错误(显然是在libc中),但我不相信这有关系。 - mvds
很好的点子 @Shawn。我从来没有注意到runOnUiThread()函数。有趣的是,我检查了源代码,发现runOnUiThread使用Handler来实现。所以@mvds,两者之间的工作不是巧合,它们在幕后使用相同的方法。 - xandy
对我来说仍然不清楚的是为什么@mdvs的解决方案不起作用。Google在http://developer.android.com/resources/articles/painless-threading.html上引用了View.post(Runnable),这就是他使用的方法,而Javadoc中说:“将Runnable添加到消息队列中。 Runnable将在用户界面线程上运行。” - Shawn Lauzon
1
我和mvds有完全相同的问题。是的,它也像三分之一那样工作,但当我添加了更多Log.i(...)时,它变得更好了,大约七分之一,当我去掉那些Log.i()时,它又恢复到三分之一....所以我不明白为什么 - 但似乎当代码变得"慢"了一点时,View.post()就起作用了!这是SDK中的一个错误吗? - hungson175
1
更新:我只需传递一个Handler,并使用handler.post() - 它完美地工作!还有一件事:当将Activity传递给线程时要小心 - 这可能会导致内存泄漏(在我的情况下,我使用线程从Web加载图像,并且该线程应该是Singleton - 静态和指向Activity的指针:就像http://developer.android.com/resources/articles/avoiding-memory-leaks.html中所述,这很危险。 - hungson175

3
使用以下代码,可以随时在任何地方发布您的代码到主线程,但不依赖于任何ContextActivity。这可以防止view.getHandler()故障或繁琐的onAttachedToWindow()等问题。
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            //TODO
        }
    });

1

我曾经遇到过同样的问题,使用view.getHandler()也失败了,因为处理程序不存在。 runOnUiThread()解决了这个问题。大概这确实会对UI进行一些排队,直到准备就绪。

对我来说,原因是在基类中调用图标加载任务并迅速返回结果,以至于主类还没有建立视图(在fragment中的getView())。

我有点怀疑它可能会偶尔失败。 但现在我已经准备好了!谢谢大家。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接