BitmapFactory.Options.inBitmap causes tearing when switching ImageView bitmap often
Asked Answered
T

5

13

I've encountered a situation where I have to display images in a slideshow that switches image very fast. The sheer number of images makes me want to store the JPEG data in memory and decode them when I want to display them. To ease on the Garbage Collector, I'm using BitmapFactory.Options.inBitmap to reuse bitmaps.

Unfortunately, this causes rather severe tearing, I've tried different solutions such as synchronization, semaphores, alternating between 2-3 bitmaps, however, none seem to fix the problem.

I've set up an example project which demonstrates this issue over at GitHub; https://github.com/Berglund/android-tearing-example

I've got a thread which decodes the bitmap, sets it on the UI thread, and sleeps for 5 ms:

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();

My idea is that ImageView.setImageBitmap(Bitmap) draws the bitmap on the next vsync, however, we're probably already decoding the next bitmap when this happens, and as such, we've started modifying the bitmap pixels. Am I thinking in the right direction?

Has anyone got any tips on where to go from here?

Tanaka answered 15/3, 2013 at 13:5 Comment(2)
Is ImageView a necessary requirement ? I'd suggest SurfaceView or TextureView for high draw rates.Enface
I am facing the same tearing but in a different scenario. I cannot ask questions because of this arse stackoverflow policy. Please help me someone. It will be a great help for me.Bondman
W
4

As an alternative to your current approach, you might consider keeping the JPEG data as you are doing, but also creating a separate Bitmap for each of your images, and using the inPurgeable and inInputShareable flags. These flags allocate the backing memory for your bitmaps on a separate heap that is not directly managed by the Java garbage collector, and allow Android itself to discard the bitmap data when it has no room for it and re-decode your JPEGs on demand when required. Android has all this special-purpose code to manage bitmap data, so why not use it?

Waistcloth answered 25/3, 2013 at 4:9 Comment(2)
Now, this. If I had seen this answer earlier I'd probably award it the bounty. This solves the problem perfectly (and very easily) on my Nexus 4 so far. I'll just have to check lower end devices too.Tanaka
glad to be of assistance = )Waistcloth
C
7

You should use the onDraw() method of the ImageView since that method is called when the view needs to draw its content on screen.

I create a new class named MyImageView which extends the ImageView and override the onDraw() method which will trigger a callback to let the listener knows that this view has finished its drawing

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();
    }

}

In the MainActivity, define 3 bitmaps: one reference to the bitmap which is being used by the ImageView to draw, one for decoding and one reference to the bitmap that is recycled for the next decoding. I reuse the synchronized block from vminorov's answer, but put in different places with explanation in the code comment

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();
            }
        });

    }
}
Contuse answered 21/3, 2013 at 4:4 Comment(3)
This does seem to fix the problem, but j__m's answer solved it easier, so I'll award it the correct answer. However, as the bounty was close to ending, I awarded it to you as your solution seems to work too. If I'd seen j__m answer before the bounty went out, I'd probably give it to j__m.Tanaka
In this case, j__m deserve the bounty. I have just raised a bounty with 200 reputation and will assign it to j__m after 24 hours.Contuse
@Binh Tran, and you deserves my +1 for this!Basion
B
4

You need to do the following things in order to get rid of this problem.

  1. Add an extra bitmap to prevent situations when ui thread draws a bitmap while another thread is modifying it.
  2. Implement threads synchronization to prevent situations when background thread tries to decode a new bitmap, but the previous one wasn't shown by the ui thread.

I've modified your code a bit and now it works fine for me.

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();
            }
        });
    }
}
Basion answered 18/3, 2013 at 15:32 Comment(2)
In fact, the method setImageBitmap() only pass the bitmap object to the ImageView and trigger the View to invalidate, at this point no drawing has been started. So it may lead to bypassing some frames if the decoding runs faster then the displaying.Contuse
Very good try, I do see improvements - but unfortunately, it doesn't fix the problem 100% because of the reasons Binh Tran gives.Tanaka
W
4

As an alternative to your current approach, you might consider keeping the JPEG data as you are doing, but also creating a separate Bitmap for each of your images, and using the inPurgeable and inInputShareable flags. These flags allocate the backing memory for your bitmaps on a separate heap that is not directly managed by the Java garbage collector, and allow Android itself to discard the bitmap data when it has no room for it and re-decode your JPEGs on demand when required. Android has all this special-purpose code to manage bitmap data, so why not use it?

Waistcloth answered 25/3, 2013 at 4:9 Comment(2)
Now, this. If I had seen this answer earlier I'd probably award it the bounty. This solves the problem perfectly (and very easily) on my Nexus 4 so far. I'll just have to check lower end devices too.Tanaka
glad to be of assistance = )Waistcloth
J
0

In the BM.decode(resource... is the network involved?

If yes then u need to optimize the look-ahead connection and data transport across the net connection as well as your work optimizing bitmaps and memory.That can mean becoming adept at low latency or async transport using your connect protocol (http i guess). Make sure that you dont transport more data than you need? Bitmap decode can often discard 80% of the pixels in creating an optimized object to fill a local view.

If the data intended for the bitmaps are already local and there are not concerns about network latency then just focus on reserving a collection type DStructure(listArray) to hold the fragments that the UI will swap on the page-forward, page-back events.

If your jpegs ( pngs are lossless with bitmap ops IMO ) are around 100k each you can just use a std adapter to load them to fragments. If they are alot larger , then you will have to figure out the bitmap 'compress' option to use with the decode in order not to waste alot of memory on your fragment data structure.

if you need a theadpool in order to optimize the bitmap creation, then do that to remove any latency involved at that step.

Im not sure that it works, but if you want to get more complicated, you could look at putting a circular buffer or something underneath the listArray that collaborates with the adapter??

IMO - once you have the structure, the transaction switching among fragments as you page should be very fast. I have direct experience with about 6 pics in memory each with size around 200k and its fast at the page-fwd, page-back.

I used this app as a framework , focusing on the 'page-viewer' example.

Jumpoff answered 18/3, 2013 at 13:44 Comment(2)
Some good points in here. However, I don't entirely understand what you mean with fragments. Are you suggesting that I switch between fragments, whom each preload a single image? I think that'd give too much overhead creating fragments and switching between them.Tanaka
developer.android.com/training/displaying-bitmaps/… . you can store bitmaps in a collection for pageview and use an adapter to handle paging. this example just stores imgRefId in the collection.Jumpoff
A
0

It's related to image caching, asycTask processing, background download from net etc. Please read this page: http://developer.android.com/training/displaying-bitmaps/index.html

If you download and look into the sample project bitmapfun on that page, I trust it will solve all your problem. That's a perfect sample.

Agraphia answered 22/3, 2013 at 1:55 Comment(1)
Sorry, but the bitmapfun-project has very little to do with displaying bitmaps quickly and maintaining performance.Tanaka

© 2022 - 2024 — McMap. All rights reserved.