BitmapFactory.Options.inBitmap在频繁切换ImageView位图时会导致撕裂。

13
我遇到了一种情况,需要在幻灯片中快速切换图片显示。由于图片数量庞大,我想将JPEG数据存储在内存中,并在需要显示时解码它们。为了减轻垃圾收集器的负担,我使用BitmapFactory.Options.inBitmap来重用位图。
不幸的是,这会导致相当严重的撕裂现象。我尝试了不同的解决方案,如同步、信号量、在2-3个位图之间交替使用,但都无法解决问题。
我在GitHub上设置了一个示例项目,演示了这个问题:https://github.com/Berglund/android-tearing-example 我有一个线程,解码位图,在UI线程上设置它,并休眠5毫秒:
Runnable runnable = new Runnable() {
@Override
public void run() {
        while(true) {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inSampleSize = 1;

            if(bitmap != null) {
                options.inBitmap = bitmap;
            }

            bitmap = BitmapFactory.decodeResource(getResources(), images.get(position), options);

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });

            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {}

            position++;
            if(position >= images.size())
                position = 0;
        }
    }
};
Thread t = new Thread(runnable);
t.start();

我的想法是ImageView.setImageBitmap(Bitmap)在下一个垂直同步时绘制位图,然而,当这发生时,我们可能已经解码了下一个位图,并且因此已经开始修改位图像素。我方向正确吗?

有人有任何提示从这里去哪里吗?


ImageView是必需的吗?我建议使用SurfaceViewTextureView来实现高绘制速率。 - S.D.
我遇到了相同的撕裂问题,但是情境不同。由于这个愚蠢的stackoverflow政策,我无法提问。请有人帮助我。这对我来说将是一个巨大的帮助。 - therealprashant
5个回答

7

您应该使用ImageView的onDraw()方法,因为当视图需要在屏幕上绘制其内容时,该方法会被调用。

我创建了一个名为MyImageView的新类,它扩展了ImageView并重写了onDraw()方法,这将触发回调以让监听器知道此视图已完成其绘制。

public class MyImageView extends ImageView {

    private OnDrawFinishedListener mDrawFinishedListener;

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDrawFinishedListener != null) {
            mDrawFinishedListener.onOnDrawFinish();
        }
    }

    public void setOnDrawFinishedListener(OnDrawFinishedListener listener) {
        mDrawFinishedListener = listener;
    }

    public interface OnDrawFinishedListener {
        public void onOnDrawFinish();
    }

}

在MainActivity中,定义3个位图:一个引用ImageView正在使用的位图进行绘制,一个用于解码,并且一个引用被回收以供下一次解码使用的位图。我重复使用了vminorov答案中的同步块,但在代码注释中加入了不同的位置解释。
public class MainActivity extends Activity {

    private Bitmap mDecodingBitmap;
    private Bitmap mShowingBitmap;
    private Bitmap mRecycledBitmap;

    private final Object lock = new Object();

    private volatile boolean ready = true;

    ArrayList<Integer> images = new ArrayList<Integer>();
    int position = 0;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        images.add(R.drawable.black);
        images.add(R.drawable.blue);
        images.add(R.drawable.green);
        images.add(R.drawable.grey);
        images.add(R.drawable.orange);
        images.add(R.drawable.pink);
        images.add(R.drawable.red);
        images.add(R.drawable.white);
        images.add(R.drawable.yellow);

        final MyImageView imageView = (MyImageView) findViewById(R.id.image);
        imageView.setOnDrawFinishedListener(new OnDrawFinishedListener() {

            @Override
            public void onOnDrawFinish() {
                /*
                 * The ImageView has finished its drawing, now we can recycle
                 * the bitmap and use the new one for the next drawing
                 */
                mRecycledBitmap = mShowingBitmap;
                mShowingBitmap = null;
                synchronized (lock) {
                    ready = true;
                    lock.notifyAll();
                }
            }
        });

        final Button goButton = (Button) findViewById(R.id.button);

        goButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        while (true) {
                            BitmapFactory.Options options = new BitmapFactory.Options();
                            options.inSampleSize = 1;

                            if (mDecodingBitmap != null) {
                                options.inBitmap = mDecodingBitmap;
                            }

                            mDecodingBitmap = BitmapFactory.decodeResource(
                                    getResources(), images.get(position),
                                    options);

                            /*
                             * If you want the images display in order and none
                             * of them is bypassed then you should stay here and
                             * wait until the ImageView finishes displaying the
                             * last bitmap, if not, remove synchronized block.
                             * 
                             * It's better if we put the lock here (after the
                             * decoding is done) so that the image is ready to
                             * pass to the ImageView when this thread resume.
                             */
                            synchronized (lock) {
                                while (!ready) {
                                    try {
                                        lock.wait();
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                }
                                ready = false;
                            }

                            if (mShowingBitmap == null) {
                                mShowingBitmap = mDecodingBitmap;
                                mDecodingBitmap = mRecycledBitmap;
                            }

                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    if (mShowingBitmap != null) {
                                        imageView
                                                .setImageBitmap(mShowingBitmap);
                                        /*
                                         * At this point, nothing has been drawn
                                         * yet, only passing the data to the
                                         * ImageView and trigger the view to
                                         * invalidate
                                         */
                                    }
                                }
                            });

                            try {
                                Thread.sleep(5);
                            } catch (InterruptedException e) {
                            }

                            position++;
                            if (position >= images.size())
                                position = 0;
                        }
                    }
                };
                Thread t = new Thread(runnable);
                t.start();
            }
        });

    }
}

这似乎解决了问题,但是j__m的答案更容易解决,所以我会把最佳答案授予给他。然而,由于悬赏即将到期,我把它授予了你,因为你的解决方案似乎也有效。如果在悬赏发布之前看到了j__m的回答,我可能会把它给j__m。 - Joakim Berglund
2
在这种情况下,j__m 值得获得赏金。我刚刚使用 200 声望值发起了一项赏金,并将在 24 小时后将其分配给 j__m。 - Binh Tran
@Binh Tran,你做得很棒!我给你点赞! - Vladimir Mironov

4

为了解决这个问题,你需要完成以下几个步骤:

  1. 添加额外的位图以防止ui线程在另一个线程修改它时绘制位图。
  2. 实现线程同步以防止后台线程尝试解码新位图,但先前的位图尚未被ui线程显示。

我稍微修改了你的代码,现在它对我来说可以正常工作了。

package com.example.TearingExample;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import java.util.ArrayList;

public class MainActivity extends Activity {
    ArrayList<Integer> images = new ArrayList<Integer>();

    private Bitmap[] buffers = new Bitmap[2];
    private volatile Bitmap current;

    private final Object lock = new Object();
    private volatile boolean ready = true;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        images.add(R.drawable.black);
        images.add(R.drawable.blue);
        images.add(R.drawable.green);
        images.add(R.drawable.grey);
        images.add(R.drawable.orange);
        images.add(R.drawable.pink);
        images.add(R.drawable.red);
        images.add(R.drawable.white);
        images.add(R.drawable.yellow);

        final ImageView imageView = (ImageView) findViewById(R.id.image);
        final Button goButton = (Button) findViewById(R.id.button);

        goButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        int position = 0;
                        int index = 0;

                        while (true) {
                            try {
                                synchronized (lock) {
                                    while (!ready) {
                                        lock.wait();
                                    }
                                    ready = false;
                                }

                                BitmapFactory.Options options = new BitmapFactory.Options();

                                options.inSampleSize = 1;
                                options.inBitmap = buffers[index];

                                buffers[index] = BitmapFactory.decodeResource(getResources(), images.get(position), options);
                                current = buffers[index];

                                runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        imageView.setImageBitmap(current);
                                        synchronized (lock) {
                                            ready = true;
                                            lock.notifyAll();
                                        }
                                    }
                                });

                                position = (position + 1) % images.size();
                                index = (index + 1) % buffers.length;

                                Thread.sleep(5);
                            } catch (InterruptedException ignore) {
                            }
                        }
                    }
                };
                Thread t = new Thread(runnable);
                t.start();
            }
        });
    }
}

事实上,setImageBitmap() 方法仅将位图对象传递给 ImageView 并触发 View 无效化,此时尚未开始绘制。因此,如果解码速度比显示速度快,则可能会导致跳过某些帧。 - Binh Tran
非常好的尝试,我确实看到了改进 - 但不幸的是,由于Binh Tran提出的原因,它并不能完全解决问题。 - Joakim Berglund

4
作为一种替代您当前方法的方式,您可以考虑保留JPEG数据,同时为每个图像创建一个单独的位图,并使用inPurgeableinInputShareable标志。这些标志将为您的位图分配备份内存,该内存不受Java垃圾收集器直接管理,并允许Android在没有足够空间时丢弃位图数据,并在需要时重新解码JPEG。Android具有专门管理位图数据的代码,那么为什么不使用它呢?

现在,如果我早些看到这个答案,我可能会把赏金给它。这个解决方案完美地解决了我的问题(而且非常容易),在我的Nexus 4上迄今为止都很好用。我只需要检查一下低端设备就行了。 - Joakim Berglund
很高兴能够提供帮助 = ) - j__m

0

在BM.decode(resource...中是否涉及网络?

如果是的话,您需要优化前瞻性连接和数据传输,以及优化位图和内存。这可能意味着要熟练掌握低延迟或异步传输,使用您的连接协议(我猜是http)。确保不要传输比您需要的更多的数据。位图解码通常可以丢弃80%的像素,从而创建一个优化的对象来填充本地视图。

如果用于位图的数据已经是本地的,并且没有关于网络延迟的问题,那么只需专注于保留一种集合类型DStructure(listArray),以容纳UI将在页面向前、向后事件上交换的片段。

如果您的JPEG(PNG是无损的,对于位图操作来说)每个约为100k,您可以使用标准适配器将它们加载到片段中。如果它们更大,则必须找出与解码一起使用的位图“压缩”选项,以便不会在片段数据结构上浪费大量内存。

如果您需要线程池以优化位图创建,则可以这样做以消除该步骤涉及的任何延迟。

我不确定它是否有效,但如果你想让它更加复杂,可以考虑在listArray下面放置一个循环缓冲区或其他协作适配器的东西?

在我看来-一旦有了结构,页面之间的事务切换应该非常快速。我直接使用了大约6个内存中的图片,每个大小约为200k,并且在向前翻页和向后翻页时都很快。

我使用 this app 作为框架,专注于“页面查看器”示例。


这里有一些好的观点。然而,我并不完全理解你所说的“片段”是什么意思。你是在建议我在片段之间切换,每个片段预加载一个单独的图像吗?我认为这会给创建片段和在它们之间切换带来太多的开销。 - Joakim Berglund
你可以将位图存储在一个集合中,以便于页面视图,并使用适配器来处理分页。这个例子只是在集合中存储imgRefId。在http://developer.android.com/training/displaying-bitmaps/display-bitmap.html#gridview上有更多信息。 - Robert Rowntree

0

抱歉,但bitmapfun项目与快速显示位图和维持性能几乎没有关系。 - Joakim Berglund

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