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) は次の vsync でビットマップを描画しますが、おそらくこれが発生したときに次のビットマップを既にデコードしているため、ビットマップ ピクセルの変更を開始しました。私は正しい方向に考えていますか?

ここからどこへ行くべきかについてのヒントはありますか?

4

5 に答える 5

7

ビューがそのコンテンツを画面に描画する必要があるときにそのメソッドが呼び出されるため、ImageView の onDraw() メソッドを使用する必要があります。

ImageView を拡張する MyImageView という名前の新しいクラスを作成し、このビューが描画を終了したことをリスナーに知らせるコールバックをトリガーする 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 つのビットマップを定義します。1 つは ImageView が描画に使用するビットマップへの参照、もう 1 つはデコード用、もう 1 つは次のデコードのために再利用されるビットマップへの参照です。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();
            }
        });

    }
}
于 2013-03-21T04:04:05.877 に答える
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();
            }
        });
    }
}
于 2013-03-18T15:32:38.183 に答える
4

現在のアプローチの代替として、JPEG データをそのままにしておくことを検討することもできますが、画像ごとに個別のビットマップを作成し、inPurgeableおよびinInputShareableフラグを使用することもできます。これらのフラグは、Java ガベージ コレクターによって直接管理されない別のヒープにビットマップのバッキング メモリを割り当て、ビットマップ データを格納する余地がない場合に Android 自体がビットマップ データを破棄し、必要に応じてオンデマンドで JPEG を再デコードできるようにします。 . Android には、ビットマップ データを管理するためのこのような専用コードがすべて含まれているので、それを使用しない理由はありません。

于 2013-03-25T04:09:23.210 に答える
0

これは、画像のキャッシュ、asycTask処理、ネットからのバックグラウンドダウンロードなどに関連しています。次のページをお読みください: http ://developer.android.com/training/displaying-bitmaps/index.html

そのページのサンプルプロジェクトbitmapfunをダウンロードして調べると、すべての問題が解決すると信じています。それは完璧なサンプルです。

于 2013-03-22T01:55:39.743 に答える
0

BM.decode(resource... にネットワークが関係していますか?

はいの場合は、ネット接続を介した先読み接続とデータ転送、およびビットマップとメモリを最適化する作業を最適化する必要があります。これは、接続プロトコルを使用して低遅延または非同期転送に熟達することを意味します (http だと思います)。必要以上のデータを転送しないようにしてください。ビットマップ デコードでは、ローカル ビューを埋めるために最適化されたオブジェクトを作成する際に、ピクセルの 80% が破棄されることがよくあります。

ビットマップ用のデータが既にローカルにあり、ネットワークの待機時間が問題にならない場合は、コレクション タイプ DStructure(listArray) を予約して、UI がページ送りイベントとページ戻しイベントで交換するフラグメントを保持することに集中してください。

jpeg (ビットマップ ops IMO では png はロスレス) がそれぞれ約 100k の場合、std アダプターを使用してそれらをフラグメントにロードすることができます。それらが非常に大きい場合は、フラグメント データ構造で大量のメモリを浪費しないように、デコードで使用するビットマップの「圧縮」オプションを理解する必要があります。

ビットマップの作成を最適化するために adpool が必要な場合は、それを実行して、そのステップで発生する遅延を取り除きます。

それが機能するかどうかはわかりませんが、もっと複雑にしたい場合は、アダプターと連携する listArray の下に循環バッファーまたは何かを配置することを検討できますか??

IMO - 構造ができたら、ページング時のフラグメント間のトランザクションの切り替えは非常に高速になるはずです。私は、サイズが約 200k で、ページ送り、ページバックで高速なメモリ内の約 6 つの写真を直接経験しています。

このアプリをフレームワークとして使用し、「ページ ビューアー」の例に焦点を当てました。

于 2013-03-18T13:44:09.260 に答える