我遇到过一种情况,我必须以幻灯片形式显示图像,以便快速切换图像。大量的图像使我想要将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上绘制位图,但是,当发生这种情况时,我们可能已经解码了下一个位图,因此,我们已经开始修改位图像素。我在想正确的方向吗?
有没有人有关于从哪里去的提示?
答案 0 :(得分: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的答案中重用了synchronized块,但是在代码注释中放入了不同的地方并进行了解释
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();
}
});
}
}
答案 1 :(得分:4)
您需要执行以下操作才能解决此问题。
我已经修改了你的代码,现在它对我来说很好。
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();
}
});
}
}
答案 2 :(得分:4)
作为当前方法的替代方法,您可以考虑保留JPEG数据,同时为每个图像创建单独的位图,并使用inPurgeable
和inInputShareable
标记。这些标志在不由Java垃圾收集器直接管理的单独堆上为您的位图分配后备内存,并允许Android本身在没有空间时丢弃位图数据,并在需要时根据需要重新解码您的JPEG 。 Android拥有所有这些用于管理位图数据的专用代码,为什么不使用它呢?
答案 3 :(得分:0)
在BM.decode中(资源... 是网络涉及?
如果是,那么你需要优化网络连接的前瞻连接和数据传输,以及优化位图和内存的工作。这可能意味着使用你的连接协议变得擅长低延迟或异步传输(http i猜测)。确保您不会传输超出您需要的数据?在创建优化对象以填充本地视图时,位图解码通常可以丢弃80%的像素。
如果用于位图的数据已经是本地的,并且不存在网络延迟问题,那么只需关注保留集合类型DStructure(listArray)来保存UI将在页面转发,页面上交换的片段 - 回事件。
如果你的jpegs(pngs与位图操作IMO无损)大约是100k,你可以使用std适配器将它们加载到片段。如果它们更大,那么你必须找出用于解码的位图'compress'选项,以免在片段数据结构上浪费大量内存。
如果您需要一个theadpool来优化位图创建,那么这样做可以消除该步骤中涉及的任何延迟。
我不确定它是否有效,但是如果你想变得更复杂,你可以看一下在listArray下面放一个循环缓冲区或与适配器协作的东西?
IMO - 一旦你拥有了这个结构,你在页面中切换的事务应该非常快。我有大约6张图片在内存中的直接经验,每张图片的大小约为200k,而它的页面返回页面速度很快。
我使用this app作为框架,专注于“页面查看器”示例。
答案 4 :(得分:0)
它与图像缓存,asycTask处理,网络后台下载等有关。 请阅读本页: http://developer.android.com/training/displaying-bitmaps/index.html
如果您下载并查看该页面上的示例项目bitmapfun,我相信它将解决您的所有问题。这是一个完美的样本。