BitmapFactory.Options.inBitmap вызывает разрыв при частом переключении растрового изображения ImageView
Я столкнулся с ситуацией, когда мне нужно отображать изображения в слайд-шоу, которое очень быстро переключает изображение. Большое количество изображений заставляет меня сохранять данные JPEG в памяти и декодировать их, когда я хочу их отобразить. Чтобы облегчить сборщик мусора, я использую BitmapFactory.Options.inBitmap для повторного использования растровых изображений.
К сожалению, это вызывает довольно сильные разрывы, я пробовал разные решения, такие как синхронизация, семафоры, чередование 2-3 битовых карт, но, похоже, ни одно из них не решило проблему.
Я создал пример проекта, который демонстрирует эту проблему на GitHub; https://github.com/Berglund/android-tearing-example
У меня есть поток, который декодирует растровое изображение, устанавливает его в потоке пользовательского интерфейса и спит в течение 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, однако, мы, вероятно, уже декодируем следующее растровое изображение, когда это происходит, и поэтому мы начали изменять пиксели растрового изображения. Думаю ли я в правильном направлении?
Кто-нибудь получил какие-либо советы о том, куда идти отсюда?
5 ответов
В качестве альтернативы вашему текущему подходу вы можете рассмотреть возможность сохранения данных JPEG в том виде, в каком вы делаете, но также создать отдельное растровое изображение для каждого из ваших изображений и использовать inPurgeable
а также inInputShareable
флаги. Эти флаги распределяют резервную память для ваших растровых изображений в отдельной куче, которая не управляется непосредственно сборщиком мусора Java, и позволяют самому Android отбрасывать растровые данные, когда для них нет места, и при необходимости повторно декодировать ваши JPEG-файлы., Android имеет весь этот специальный код для управления растровыми данными, так почему бы не использовать его?
Вам следует использовать метод onDraw() в ImageView, так как этот метод вызывается, когда представление должно рисовать свое содержимое на экране.
Я создаю новый класс с именем 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 для рисования, одно для декодирования и одну ссылку на растровое изображение, которое повторно используется для следующего декодирования. Я повторно использую синхронизированный блок из ответа вминорова, но ставлю в разных местах с пояснениями в комментарии к коду
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();
}
});
}
}
Вам нужно сделать следующие вещи, чтобы избавиться от этой проблемы.
- Добавьте дополнительное растровое изображение, чтобы предотвратить ситуации, когда поток пользовательского интерфейса рисует растровое изображение, в то время как другой поток изменяет его.
- Реализуйте синхронизацию потоков, чтобы предотвратить ситуации, когда фоновый поток пытается декодировать новое растровое изображение, но предыдущий поток не отображался потоком пользовательского интерфейса.
Я немного изменил ваш код, и теперь он отлично работает для меня.
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();
}
});
}
}
Это связано с кэшированием изображений, обработкой asycTask, загрузкой фона из сети и т. Д. Пожалуйста, прочтите эту страницу: http://developer.android.com/training/displaying-bitmaps/index.html
Если вы скачаете и посмотрите пример проекта bitmapfun на этой странице, я надеюсь, что он решит все ваши проблемы. Это идеальный образец.
В BM.decode(ресурсе... участвует ли сеть?
Если да, то вам нужно оптимизировать упреждающее соединение и передачу данных по сетевому соединению, а также оптимизировать растровые изображения и память для вашей работы. Это может означать, что вы станете специалистом с низкой задержкой или асинхронным транспортом, используя ваш протокол соединения (http, я полагаю). Убедитесь, что вы не переносите больше данных, чем вам нужно? Растровое декодирование часто может отбрасывать 80% пикселей при создании оптимизированного объекта для заполнения локального представления.
Если данные, предназначенные для растровых изображений, уже являются локальными и нет проблем с задержкой в сети, просто сконцентрируйтесь на резервировании типа коллекции DStructure(listArray) для хранения фрагментов, которые пользовательский интерфейс поменяет местами на событиях перехода на страницу вперед и назад.
Если ваши jpegs ( pngs без потерь с IMO растровых операций) имеют размер около 100 Кб каждый, вы можете просто использовать адаптер std для загрузки их во фрагменты. Если они намного больше, вам нужно будет выбрать опцию "сжатия" растрового изображения для использования с декодированием, чтобы не тратить много памяти на структуру данных фрагмента.
если вам нужна theadpool для оптимизации создания растрового изображения, то сделайте это, чтобы удалить любую задержку, связанную с этим шагом.
Я не уверен, что это работает, но если вы хотите усложнить работу, вы можете посмотреть, как поместить круговой буфер или что-то под listArray, который взаимодействует с адаптером??
IMO - когда у вас есть структура, транзакция, переключающаяся между фрагментами на странице, должна быть очень быстрой. У меня есть непосредственный опыт работы с примерно 6 фотографиями в памяти, каждый размером около 200 КБ и быстрым на page-fwd, page-back.
Я использовал это приложение в качестве основы, ориентируясь на пример "просмотра страниц".