3

最近几天我一直在与一个奇怪的问题作斗争。我有一个自定义 ExpandableListAdapter,其中每一行都包含一个 ImageView 等。我有一个类可以处理从它们可能驻留的多个位置(磁盘缓存、应用程序数据、远程服务器等)异步加载图像。在我的适配器的 getView 方法中,我将返回 View 的责任委托给列表 Item 本身(我的组列表有多种行类型)。我要求图像加载如下:

final ImageView thumb = holder.thumb;
holder.token = mFetcher.fetchThumb(mImage.id, new BitmapFetcher.Callback() {

            @Override
            public void onBitmap(final Bitmap b) {
                thumb.post(new Runnable() {
                    @Override
                    public void run() {
                        thumb.setImageBitmap(b);
                    }
                });
            }

            @Override
            public void onFailure() {
            }
        });

是的,这很丑陋,但我决定反对一些让 BitmapFetcher.Callback 默认在 UI 线程上执行其方法的合同。

无论如何,当我加载包含 ExpandableListView 的 Activity 时,列表中的不同行通常会丢失拇指图像。重新加载 Activity 可能会导致显示一些丢失的拇指,但以前显示的其他拇指可能不再显示。据我所知,这种行为是非常随机的。滚动 ListView 以使缺少图像的行被回收导致新的拇指图像(当回收的行再次显示时)加载正常。回滚到先前包含缺失图像的行会导致出现缺失图像。我可以确认所有图像都从我的 BitmapFetcher (mFetcher) 类中正确加载。我还应该提到我在其他地方加载了其他图像。每隔一段时间,它们也不会出现。

拔掉大部分头发后,我发现发生了变化:

thumb.post(new Runnable() {

至:

mExpListView.post(new Runnable() {

解决了这个问题。我最初认为这个问题可能会发生,因为我使用的是对视图的最终引用,但是应用程序中的其他位置使用对视图的非最终引用来发布消息,而且,正如我所提到的,有时这些不起作用. 我最终更改了所有内容以使用 Activity 的 runOnUiThread() 方法(以及我自己在 Fragments 中的 getUiThreadRunner().execute 方法),这似乎解决了所有问题。

所以我的问题仍然存在,在什么情况下 View.post() 无法以正确的顺序将可运行对象传递到关联的 ViewRoot 的消息队列?或者,invalidate() 可能在从 getView 返回 View 之前发生,因此在它被放置在可以从根 View 访问的 ViewGroup 之前发生。这些确实是我能想到的唯一会阻止图像出现的情况。我可以保证至少在 onStart 完成执行之前不会发生这些调用。此外,即使尚未将其附加到窗口,也可以将其发布到视图:

// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(attachInfo.mHandler);

(在 performTraversal 中)。runOnUiThread 和 post 之间的唯一区别似乎是 Activity 具有与 ViewRootImpl 不同的 Handler。

活动:

final Handler mHandler = new Handler();

而在 ViewRootImpl 中:

final ViewRootHandler handler = new ViewRootHandler();

但是,如果两个处理程序都是在同一个线程中构造的(或使用同一个 Looper),这应该不是问题。这让我想知道是否确实存在使尚未添加到层次结构中的视图无效()的问题。对于这种情况,invalidate 应该 1. 如果它不可见,则不做任何事情,或者 2. 仅对发生的下一个 performTraversal() 有效。

View.invalidate() 检查一个不错的私有方法,它没有记录在案,称为 skipInvalidate():

/**
 * Do not invalidate views which are not visible and which are not running an animation. They
 * will not get drawn and they should not set dirty flags as if they will be drawn
 */
private boolean skipInvalidate() {
    return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
            (!(mParent instanceof ViewGroup) ||
                    !((ViewGroup) mParent).isViewTransitioning(this));
}

看起来1号更准确!但是,我认为这仅与视图的 VISIBILITY 属性有关。那么,如果无法从 ViewRoot 访问视图,则认为视图被视为不可见是否准确?还是 VISIBILITY 属性不受视图容器的影响?如果是前者(我怀疑是这样),就会引起关注。我使用 Activity.runOnUiThread 并不能解决问题。它只是碰巧起作用,因为 invalidate() 调用被发送到不同的处理程序并稍后执行(在 getView 返回之后以及在添加行并使其在屏幕上可见之后)。有没有其他人遇到过这个问题?有没有好的解决方案?

4

2 回答 2

1

嘿大卫我很久以前遇到过类似的问题。的基本要求view.post(Runnable r)view应将 附加到要执行的 Runnable 的窗口。但是,由于您在第一种情况下异步加载图像,因此在发出imageView发布请求时可能没有附加到窗口,因此,某些图像无法加载。

引用早期版本的文档:

View.post():将 Runnable 添加到消息队列中。runnable 将在用户界面线程上运行。仅当此视图附加到窗口时,才能从 UI 线程外部调用此方法。

切换到下一个问题,处理这种情况的最佳解决方案是什么?

无法评论最佳解决方案。但是,我认为两者handler.post()activity.runOnUIThread()很好。因为,它们基本上在主线程队列中发布可运行,而不管任何事情,一般来说,显示列表行的请求将在我们的thumb.post(). 因此,在大多数情况下,它们可能完美无缺。(至少我从来没有遇到过他们的问题!)。然而。如果您找到更好的解决方案,请与我分享。

于 2013-09-06T18:23:57.253 回答
0

试试这个: setBitmap() 像这样:

runOnUiThread(new Runnable() {

            @Override
            public void run() {
                thumb.setImageBitmap(b);
            }
        });
于 2013-08-24T04:31:52.967 回答