【问题标题】:BitmapFactory.Options.inBitmap causes tearing when switching ImageView bitmap often经常切换ImageView位图时BitmapFactory.Options.inBitmap导致撕裂
【发布时间】:2013-03-04 05:15:15
【问题描述】:

我遇到了一种情况,即我必须在幻灯片中显示图像,而且切换图像的速度非常快。庞大的图像数量让我想将 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) 在下一个 vsync 上绘制位图,但是,当这种情况发生时,我们可能已经在解码下一个位图,因此,我们已经开始修改位图像素。我的想法是否正确?

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

【问题讨论】:

  • ImageView 是必要条件吗?我建议SurfaceViewTextureView 获得高抽取率。
  • 我面临同样的撕裂,但情况不同。由于这个 ass stackoverflow 政策,我无法提问。请帮助我的人。这对我有很大的帮助。

标签: android bitmap bitmapfactory tearing


【解决方案1】:

您应该使用 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。
  • 在这种情况下,j__m 应该得到赏金。我刚刚筹集了 200 声望的赏金,并将在 24 小时后将其分配给 j__m。
  • @Binh Tran,你值得我为此 +1!
【解决方案2】:

作为当前方法的替代方案,您可以考虑保留 JPEG 数据,但也为每个图像创建单独的位图,并使用 inPurgeableinInputShareable 标志。这些标志在不由 Java 垃圾收集器直接管理的单独堆上为您的位图分配后备内存,并允许 Android 本身在没有空间时丢弃位图数据,并在需要时按需重新解码您的 JPEG . Android 有所有这些专用代码来管理位图数据,为什么不使用它呢?

【讨论】:

  • 现在,这个。如果我早些时候看到这个答案,我可能会奖励它。到目前为止,这在我的 Nexus 4 上完美地(并且非常容易地)解决了这个问题。我也只需要检查低端设备。
  • 很高兴为您提供帮助 = )
【解决方案3】:

为了摆脱这个问题,你需要做以下事情。

  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 给出的原因,它并不能 100% 解决问题。
【解决方案4】:

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

如果是,那么您需要优化通过网络连接的前瞻连接和数据传输以及优化位图和内存的工作。这可能意味着使用您的连接协议(http i猜测)。确保您传输的数据不超过您的需要?在创建优化对象以填充局部视图时,位图解码通常会丢弃 80% 的像素。

如果用于位图的数据已经是本地的并且不担心网络延迟,那么只需专注于保留集合类型 DStructure(listArray) 来保存 UI 将在页面转发、页面交换时交换的片段返回事件。

如果您的 jpeg(png 对位图操作 IMO 是无损的)每个约为 100k,您可以使用 std 适配器将它们加载到片段中。如果它们更大,那么您将不得不找出与解码一起使用的位图“压缩”选项,以免在片段数据结构上浪费大量内存。

如果您需要一个 theadpool 来优化位图创建,那么请这样做以消除该步骤所涉及的任何延迟。

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

IMO - 一旦你有了结构,页面之间的事务切换应该非常快。我在内存中有大约 6 张图片的直接经验,每张图片的大小约为 200k,并且在 page-fwd、page-back 处速度很快。

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

【讨论】:

  • 这里有一些优点。但是,我不完全理解您对片段的含义。您是否建议我在片段之间切换,每个片段都预加载一个图像?我认为创建片段和在它们之间切换会产生太多开销。
  • developer.android.com/training/displaying-bitmaps/… 。您可以将位图存储在页面视图的集合中,并使用适配器来处理分页。此示例仅将 imgRefId 存储在集合中。
【解决方案5】:

它与图像缓存、asycTask 处理、网络后台下载等有关。 请阅读此页面: http://developer.android.com/training/displaying-bitmaps/index.html

如果您下载并查看该页面上的示例项目 bitmapfun,我相信它会解决您的所有问题。这是一个完美的样本。

【讨论】:

  • 抱歉,bitmapfun-project 与快速显示位图和保持性能关系不大。
猜你喜欢
  • 1970-01-01
  • 2018-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多