AndroidのBitmapキャッシュプールの使用の詳細

10517 ワード

この文書では、キャッシュを使用してUIの読み込み入力とスライドのスムーズ性を向上させる方法について説明します.この問題は、メモリキャッシュの使用、ディスクキャッシュの使用、構成変更イベントの処理などの方法で効果的に解決されます.
UIに単一の画像を表示するのは簡単ですが、一度に多くの画像を表示する必要がある場合は複雑です.多くの場合(例えば、ListView、GridView、またはView Pagerコントロールを使用する)、画面に表示されるピクチャおよび画面に表示されるピクチャの数は非常に大きい(例えば、ライブラリで大量のピクチャを参照する).
これらのコントロールでは、サブコントロールが表示されない場合、メモリの消費を減らすために、コントロールを再利用してループ表示します.同時に、ゴミ回収メカニズムは、メモリにロードされているBitmapリソースを解放します(Bitmapを強く参照していないとします).一般的にはこれが良いですが、ユーザーが画面をスライドさせるときに、UIのスムーズさと画像のロード効率を保証するために、表示する必要がある画像の繰り返し処理を避ける必要があります.メモリキャッシュとディスクキャッシュを使用すると、この問題を解決できます.キャッシュを使用すると、コントロールが処理した画像をすばやくロードできます.
この文書では、キャッシュを使用してUIの読み込み入力とスライドのスムーズ性を向上させる方法について説明します.
メモリキャッシュの使用
メモリキャッシュは画像へのアクセス速度を向上させますが、多くのメモリを消費します.LruCacheクラス(API 4より前にSupport Libraryのクラスを使用可能)はBitmapをキャッシュするのに特に適しており、最近使用したBitmapオブジェクトを強いリファレンスで保存(LinkedHashMapに保存)し、キャッシュ数が所定の値に達したときに、あまり使用しない対象を削除する.
注意:従来、メモリキャッシュを実装する一般的な方法はSoftReferenceまたはWeakReference bitmapキャッシュを使用していましたが、この方法は推奨されていません.Android 2.3(API Level 9)から、ゴミ回収がソフトウェア/weakリファレンスを強制的に回収し始め、これらのキャッシュの効率が向上しなかった.また、Android 3.0(API Level 11)の前に、これらのキャッシュされたBitmapデータは最下位メモリ(native memory)に保存され、所定の条件に達してもこれらのオブジェクトは解放されず、プログラムがメモリ制限を超えてクラッシュする可能性がある.
LruCacheを使用する場合、適切なキャッシュ数パラメータを選択するには、次の要因を考慮する必要があります.
1.プログラムにどれだけのメモリがあるか2.同時に画面に何枚の画像が表示されますか?表示する画面にどのくらいの画像をキャッシュしますか?3.設備のスクリーンサイズとスクリーン密度はいくらですか.超高スクリーン密度(xhdpi、例えばGalaxy Nexus)4.デバイスが同じピクチャを表示するには、低画面密度(hdpi、例えばNexus S)デバイスよりも多くのメモリが必要です.5.画像のサイズとフォーマットは、各画像にどれだけのメモリが必要かを決定する.画像アクセスの頻度はどうですか?いくつかの画像のアクセス頻度は他の画像よりずっと高いですか?そうであれば、よくアクセスする画像をメモリに入れる必要があるかもしれません.7.品質と数量のバランスはどうですか.大量の低品質のピクチャを保存する場合に便利であり、必要に応じてバックグラウンドスレッドを使用して高品質のピクチャを追加する場合があります.
すべてのプログラムに適した万能レシピはありません.使用状況を分析し、独自のキャッシュポリシーを指定する必要があります.小さなキャッシュを使用すると効果的ではありませんが、大きなキャッシュを使用するとより多くのメモリが消費され、javaが発生する可能性があります.lang.OutOfMemory異常またはプログラムの他の機能で使用するためのメモリが少ない.
LruCacheキャッシュを使用する例を次に示します.
 
  
private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get memory class of this device, exceeding this amount will throw an
    // OutOfMemory exception.
    final int memClass = ((ActivityManager) context.getSystemService(
            Context.ACTIVITY_SERVICE)).getMemoryClass();

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = 1024 * 1024 * memClass / 8;

    mMemoryCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in bytes rather than number of items.
            return bitmap.getByteCount();
        }
    };
    ...
}                                                              
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}                                                              
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}


注:この例では、プログラムの1/8メモリをキャッシュに使用します.1つのnormal/hdpiデバイスでは、少なくとも4 MB(32/8)のメモリがあります.
解像度800×480のデバイスでは、フルスクリーンのGridViewに画像をすべて埋め込むと1.5 MB(800*480*4 bytes)の差が少なくなります.
のメモリなので、メモリに2.5ページの画像をキャッシュする差は多くありません.
ImageViewに画像を表示する場合は、LruCacheに存在するかどうかを確認します.存在する場合はキャッシュされたピクチャを使用し、存在しない場合はバックグラウンドスレッドを起動してピクチャをロードし、キャッシュします.
 
  
public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

BitmapWorkerTaskでは、キャッシュに新しい画像を追加する必要があります.
 
  
class BitmapWorkerTask extends AsyncTask {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

次のページでは、ディスク・キャッシュと構成変更イベントの処理の2つの方法について説明します.
ディスクキャッシュの使用
最近使用したピクチャへのアクセスでは、メモリのキャッシュ速度は速いが、ピクチャがキャッシュに存在するかどうかは分からない.GridViewのようなコントロールには、多くの画像を表示する必要がある可能性があります.すぐに画像データがキャッシュ容量を満たします.また、電話などの他のタスクによってプログラムが中断される可能性があります.プログラムがバックグラウンドにある場合、システムはこれらの画像キャッシュを認識する可能性があります.ユーザーがプログラムの使用を再開すると、これらの画像を再処理する必要があります.
この場合、ディスクキャッシュを使用して処理済みのピクチャを保存することができ、メモリキャッシュで使用できない場合、ディスクキャッシュからロードしてピクチャ処理を省略することができる.もちろん、ディスクからの画像の読み込みはメモリからの読み込みよりもかなり遅く、非UIスレッドにディスク画像をロードする必要があります.
注意:キャッシュされたピクチャが頻繁に使用される場合は、ContentProvider、例えばライブラリプログラムではこのように乾滴することが考えられます.
サンプルコードには簡単なDiskLruCache実装があります.その後、Android 4.0には、より信頼性が高く、推奨されるDiskLruCache(libcore/luni/src/main/java/libcore/io/diskLruCache.java)が含まれています.このインプリメンテーションを4.0以前のバージョンに簡単に移植できます(href=)http://www.google.com/search?q=disklrucache」>Googleは他の人がやったかどうか見てみましょう!).
ここでは、DiskLruCacheのバージョンを更新します.
 
  
private DiskLruCache mDiskCache;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
    mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
    ...
}                               
class BitmapWorkerTask extends AsyncTask {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }                              
        // Add final bitmap to caches
        addBitmapToCache(String.valueOf(imageKey, bitmap);

        return bitmap;
    }
    ...
}                               
public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }                               
    // Also add to disk cache
    if (!mDiskCache.containsKey(key)) {
        mDiskCache.put(key, bitmap);
    }
}                               
public Bitmap getBitmapFromDiskCache(String key) {
    return mDiskCache.get(key);
}                               
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
            || !Environment.isExternalStorageRemovable() ?
                    context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
    return new File(cachePath + File.separator + uniqueName);
}


UIスレッドでメモリキャッシュを検出し、バックグラウンドスレッドでディスクキャッシュを検出します.ディスク操作はUIスレッドでは実現されません.画像処理が完了すると、最終的な結果が同時に追加されます.
メモリキャッシュとディスクキャッシュで将来使用します.
構成変更イベントの処理
実行時の構成変更、例えば画面方向の変更は、Androidが実行中のActivityを破壊し、新しい構成を使用してActivityを新たに起動させる(詳細は、ここHandling Runtime Changesを参照).構成が変更されると、すべての画像が再処理され、ユーザー体験が向上しないように注意する必要があります.
幸いなことに、メモリキャッシュを使用する部分にはすでに良い画像キャッシュがあります.このキャッシュはFragment(FragmentはsetReatainInstance(true)関数で保存)によって新しいActivityに渡すことができます.Activityが再起動すると、FragmentがActivityに再添付され、このFragmentによってキャッシュオブジェクトを取得できます.
次に、Fragmentにキャッシュを保存する例を示します.
 
  
private LruCache mMemoryCache;                 
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment mRetainFragment =            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = RetainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache(cacheSize) {
            ... // Initialize cache here as usual
        }
        mRetainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}                 
class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache mRetainedCache;

    public RetainFragment() {}                 
    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
        }
        return fragment;
    }                 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}


また、Fragmentを使用するか使用しないかで、デバイスの画面方向を回転させて、特定の画像の読み込み状況を確認してみてください.