Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android官方開發文檔Training系列課程中文版:高效顯示位圖之位圖緩存

Android官方開發文檔Training系列課程中文版:高效顯示位圖之位圖緩存

編輯:關於Android編程

原文地址:http://android.xsoftlab.net/training/displaying-bitmaps/cache-bitmap.html

往UI界面中加載單張圖片的過程是很簡單的,然而如果需要在某個時刻同時加載大量的圖片,那麼這事情就有些復雜了。在很多情況下,比如使用了ListView、GridView或者是ViewPager來展示一定數量的圖片,在本質上這些情況下,屏幕的快速滑動會導致大量的圖片被集中展示在屏幕上。

類似這樣通過回收移除到屏幕之外的子View的組件會抑制內存的使用(也就是說它們本身不會濫用內存)。垃圾回收器還會釋放你所加載的位圖,假設你沒有使用任何持久化引用的話。這真是極好的,但是為了保持流暢的UI效果,你可能需要在它們每次重新返回到屏幕的時候,對它們按照常規的方式重新處理。內存緩存及磁盤緩存可以在這裡提供幫助,可以使這些組件快速的重新加載已經處理過的圖片。

這節課將會討論在加載多張圖片的時候,如何通過使用內存緩存以及磁盤緩存來使UI界面更加流暢,響應速度更快。

使用內存緩存

內存緩存提供了一種快速訪問位圖的能力,不過這會花費寶貴的內存空間。類LruCache極其適合用來處理緩存圖片的任務,它會將最近使用到的位圖的引用存放在一個LinkedHashMap對象上,並會在超過內存設計大小之前將最後一個沒有用到的成員給驅除。

Note: 在過去,使用SoftReference或者是WeakReference來緩存圖片是最受歡迎的一種緩存方式,然而卻並不推薦這麼用。在Android 2.3之後,垃圾回收器對soft/weak引用的回收更加強制,這會使得這些引用幾乎無效。此外,在Android 3.0之前,位圖的字節數據被存儲在本地內存中,可以預見這些數據是不會被釋放的,這會導致程序很容易超過自身的內存限制,然後崩潰。

為了給LruCache選擇合適的尺寸,有幾個因素應該被考慮在內:

Activity或者程序在常規狀態下的內存使用量是多少? 在同一時間最多會有多少圖片集中顯示在屏幕上?有多少內存需要為准備顯示到屏幕上的圖片所用? 設備屏幕的大小和尺寸分別是多少?在加載相同圖片數量的情況下,像Galaxy Nexus這種超高的密度(xhdpi)的設備與Nexus S(hdpi)相比則需要更大的內存。 圖片的尺寸多大?配置是什麼?加載這個位圖的時候需要花費的內存是多少? 圖片的訪問有多頻繁?會比其它位圖訪問更頻繁嗎?如果是這樣,可能你需要將它們永遠保持在內存中了,或者甚至是有多個LruCache對象來為圖片分組。 你可以在數量與質量之間取得平衡嗎?某些時候存儲大量的低質圖片是很有用處的,可能會潛在的存在一些後台任務來加載一些高質量的版本。

這裡特別沒有指定尺寸或者配置,不過這適用所有的應用程序,這取決於對內存使用情況的分析,並需要找到一個適合的解決方案。緩存設置的太小會導致無意義的額外開銷,緩存設置的太大會再次引起java.lang.OutOfMemory異常,應該將大小設置為應用的常規內存使用量之外的剩余內存之間。

下面是使用LruCache緩存位圖的一個例子:

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

Note: 在這個例子中,有八分之一的內存被分配給了緩存。在正常的設備上(hdpi)這大概是4MB(32/8)左右。一個鋪滿了圖片的GridView在全屏狀態下的800*480的設備上所占的內存大概是1.5MB(800*480*4個字節),所以這可以在內存中存儲大概2.5頁的圖像。

當加載一個位圖到ImageView上的時候,首先要檢查LruCache。如果發現了與之相匹配的,則會被用來立即更新到ImageView上,否則就會觸發一個後台線程來處理圖片:

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;
    }
    ...
}

使用磁盤緩存

內存緩存對於最近浏覽過的圖像的快速加載非常有用,然而卻不能將所有的圖像都存放在內存緩存中。像GridView這樣的組件在加載大數據集的時候可以輕易的將內存緩存填滿。程序在運行的過程中可能會被其它任務打斷,比如一個來電,這時,在後台的任務可能就會被殺死,內存緩存也會被銷毀。一旦用戶返回了界面,那麼程序就需要再次重新處理每張圖片。

那麼磁盤緩存在這些情況下就很有幫助了,它可以存儲處理過的圖片,並會輔助提升圖片的加載時間,在圖片不再在內存緩存中存在的時候。當然,在磁盤上獲取一張圖片要比內存中要慢,並且還需要開啟單獨的工作線程,這和從磁盤上讀取數據的時間一樣,都不可預估。

Note:ContentProvider可能更適合用來存放被緩存過的圖像,如果這些圖像的訪問更加頻繁的話,就像在相冊應用中的情況一樣。

從Android Source中更新的示例代碼使用了一個DiskLruCache的實現。下面是個更新後的版本,它對已有的內存緩存增加了磁盤緩存:

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 {
    @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 {
    ...
    // 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);
}

Note:因為磁盤緩存的初始化需要磁盤操作,所以這個過程不應該放在UI線程中執行。然而,這也意味著在緩存初始化之前這是個訪問的機會。為了做到這一點,需要有個lock對象來保證在緩存被初始化之前APP沒有從磁盤緩存中讀取數據。

內存緩存在UI線程中執行檢查,磁盤緩存在後台線程中執行檢查。磁盤操作絕不應該放入UI線程。當圖像處理完畢後,最終被處理過的圖片應當被添加到內存緩存及磁盤緩存中以便備用。

處理配置變更

如果在運行時發生了變更,比如屏幕的方向發生了改變,會引起Android銷毀並重啟運行中的Activity,你可能想要避免再一次處理圖像,這樣一旦配置發生了改變,可以使用戶有一個流暢快速的用戶體驗。

幸運的是,你有一個非常贊的內存緩存方案:可以使用設置了setRetainInstance(true)的Fragment,它可以將緩存傳入新的Activity實例。在activity重新創建的時候,這個被保留存在的Fragment會被重新附加在Activity上,你可以獲得原先內存緩存的訪問能力,這使得圖像可以快速的被獲得並被重新填充在ImageView對象中。

下面這個例子使用了引用LruCache的Fragment,並通過了配置更改的問題:

private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.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();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

為了測試這項輸出,試著在有和沒有Fragment的情況下旋轉設備。你應該會注意到這個過程幾乎沒有延遲。任何圖像如果沒有在內存緩存中找到,那麼這就為磁盤緩存提供了用武之地,如果都沒有的話,那麼常規的處理方法就會出場。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved