Bitmap的cache處理,包括memory cache和disk cache

翻譯至點擊打開鏈接


安卓的控件經常會顯示很多圖片,如list view or view pager: recycling the child views as they move off-screen.

垃圾回收會釋放加載的bitmapgarbage collector also frees up your loaded bitmaps, assuming you don't keep any long lived references. 

下面介紹一直內存cache和disk cache: A memory and disk cache

LruCache class is particularly well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced LinkedHashMap and evicting the least recently used member before the cache exceeds its designated size.

In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache, however this is not recommended.Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective. 

舉個栗子LRU cache,最近最少用

private LruCache<String, Bitmap> mMemoryCache;


@Override

protected void onCreate(Bundle savedInstanceState) {

    ...

    // Get max available VM memory, exceeding this amount will throw an

    // OutOfMemory exception. Stored in kilobytes as LruCache takes an

    // int in its constructor.

    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);


    // Use 1/8th of the available memory for this memory cache.

    final int cacheSize = maxMemory / 8;


    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

        @Override

        protected int sizeOf(String key, Bitmap bitmap) {

            // The cache size will be measured in kilobytes rather than

            // number of items.

            return bitmap.getByteCount() / 1024;

        }

    };

    ...

}


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的內存用於緩存圖片,On a normal/hdpi device this is a minimum of around 4MB (32/8).

A full screen GridView filled with images on a device with 800x480 resolution would use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in memory.

When loading a bitmap into an ImageView, the LruCache is checked first. If an entry is found, it is used immediately to update the ImageView, otherwise abackground thread is spawned to process the image:

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是一個異步task

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {

    ...

    // 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;

    }

    …

}

存在內存裏是不夠的,在onPause時,還需要存到disk中,

application could be interrupted by another task like a phone call, and while in the background it might be killed and the memory cache destroyed. Once the user resumes, your application has to process each image again.fetching images from disk is slower than loading from memory and should be done in a background thread, as disk read times can be unpredictable. A ContentProvider might be a more appropriate place to store cached images if they are accessed more frequently, for example in an image gallery application.

舉個栗子

private DiskLruCache mDiskLruCache;

private final Object mDiskCacheLock = new Object();

private boolean mDiskCacheStarting = true;

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

    ...

    // Initialize disk cache on background thread

    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);

    new InitDiskCacheTask().execute(cacheDir);

    ...

}


class InitDiskCacheTask extends AsyncTask<File, Void, Void> {

    @Override

    protected Void doInBackground(File... params) {

        synchronized (mDiskCacheLock) {

            File cacheDir = params[0];

            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);

            mDiskCacheStarting = false; // Finished initialization

            mDiskCacheLock.notifyAll(); // Wake any waiting threads

        }

        return null;

    }

}


class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {

    ...

    // 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(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

    synchronized (mDiskCacheLock) {

        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {

            mDiskLruCache.put(key, bitmap);

        }

    }

}


public Bitmap getBitmapFromDiskCache(String key) {

    synchronized (mDiskCacheLock) {

        // Wait while disk cache is started from background thread

        while (mDiskCacheStarting) {

            try {

                mDiskCacheLock.wait();

            } catch (InterruptedException e) {}

        }

        if (mDiskLruCache != null) {

            return mDiskLruCache.get(key);

        }

    }

    return null;

}


// 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 getDiskCacheDir(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.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||

                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :

                            context.getCacheDir().getPath();


    return new File(cachePath + File.separator + uniqueName);

}


 a lock object ensures that the app does not read from the disk cache until the cache has been initialized.

切換屏幕方向

用Fragment舉個栗子,設置fragment爲retain instance

This cache can be passed through to the new activity instance using a Fragment which is preserved by calling setRetainInstance(true)). After the activity has been recreated, this retained Fragment is reattached and you gain access to the existing cache object, allowing images to be quickly fetched and re-populated into theImageView objects.

private LruCache<String, Bitmap> mMemoryCache;


@Override

protected void onCreate(Bundle savedInstanceState) {

    ...

    RetainFragment retainFragment =

            RetainFragment.findOrCreateRetainFragment(getFragmentManager());

    mMemoryCache = retainFragment.mRetainedCache;

    if (mMemoryCache == null) {

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            ... // Initialize cache here as usual

        }

        retainFragment.mRetainedCache = mMemoryCache;

    }

    ...

}

其中setRetainInstance的源碼如下,不會去調用onDestroy和onCreate,而是調用onDetach和onAttach

 /**
     * Control whether a fragment instance is retained across Activity
     * re-creation (such as from a configuration change).  This can only
     * be used with fragments not in the back stack.  If set, the fragment
     * lifecycle will be slightly different when an activity is recreated:
     * <ul>
     * <li> {@link #onDestroy()} will not be called (but {@link #onDetach()} still
     * will be, because the fragment is being detached from its current activity).
     * <li> {@link #onCreate(Bundle)} will not be called since the fragment
     * is not being re-created.
     * <li> {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} <b>will</b>
     * still be called.
     * </ul>
     */
    public void setRetainInstance(boolean retain) {
        if (retain && mParentFragment != null) {
            throw new IllegalStateException(
                    "Can't retain fragements that are nested in other fragments");
        }
        mRetainInstance = retain;
    }


class RetainFragment extends Fragment {

    private static final String TAG = "RetainFragment";

    public LruCache<String, Bitmap> mRetainedCache;


    public RetainFragment() {}


    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {

        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);

        if (fragment == null) {

            fragment = new RetainFragment();

            fm.beginTransaction().add(fragment, TAG).commit();

        }

        return fragment;

    }


    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setRetainInstance(true);

    }

}


發佈了118 篇原創文章 · 獲贊 2 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章