在使用者介面(UI)載入一張圖片時很簡單,然而,如果你需要載入多張較大的映像,事情就會變得更加複雜,。在許多情況下(如與像的ListView GridView或ViewPager的組件),螢幕上的圖片的總數伴隨螢幕上滾動的驟然增加,且基本上是無限的。為使記憶體使用量保持在穩定範圍內,這些組件會在子view在螢幕中消失後,對其進行資源回收,記憶體回收機制會釋放掉已載入的圖片記憶體空間,所以建議你不要保持圖片的常引用,這樣做很好,為了保證頁面的流暢性和響應速度,你可能不願意在頁面返回時頻繁處理載入過圖片。通過記憶體、磁碟的緩衝就可以協助你快速載入已載入的圖片。
今天與大家分享一片的緩衝技術,利用它可以提高UI的流暢性、響應速度,給使用者好的體驗。
如何在記憶體中做緩衝?
通過記憶體緩衝可以快速載入緩衝圖片,但會消耗應用的記憶體空間。LruCache類(通過相容包可以支援到sdk4)很適合做圖片緩衝,它通過LinkedHashMap保持圖片的強引用方式儲存圖片,當緩衝空間超過設定定的限值時會釋放掉早期的緩衝。
註:在過去,常用的記憶體緩衝實現是通過SoftReference或WeakReference,但不建議這樣做。從Android2.3(API等級9)垃圾收集器開始更積極收集軟/弱引用,這使得它們相當無效。此外,在Android 3.0(API等級11)之前,儲存在native記憶體中的可見的bitmap不會被釋放,可能會導致應用程式暫時地超過其記憶體限制並崩潰。
為了給LruCache設定合適的大小,需要考慮以下幾點因素:
你的應用中空閑記憶體是多大?
你要在螢幕中一次顯示多少圖片? 你準備多少張圖片用於顯示?
裝置的螢幕大小與density 是多少?超高螢幕density的裝置(xhdpi)像Galaxy Nexus 比 Nexus S (hdpi)這樣的裝置在緩衝相同的圖片時需要更大的Cache空間。
圖片的大小和屬性及其需要佔用多少記憶體空間?
圖片的訪問頻率是多少? 是否比其他的圖片使用的頻率高?如果這樣你可能需要考慮將圖片長期存放在記憶體中或者針對不同類型的圖片使用不同的緩衝策略。
如何平衡品質與數量,有事你可能會儲存一些常用的低品質的圖片使用者顯示,然後通過非同步線程載入高品質的圖片。
圖片緩衝方案沒有固定的模式使用所有的的應用,你需要根據應用的具體應用情境進行分析,選擇合適的方案來做,緩衝太小不能發揮緩衝的優勢,太大可能佔用過多的記憶體,降低應用效能,或者發生記憶體溢出異常,
下面是一個使用LruCache的例子:
private LruCache mMemoryCache; @Overrideprotected 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); }
注意:在這個例子中,應用八分子一的記憶體配置給圖片緩衝,在普通/hdpi裝置中大約為4MB(32/8)。GirdView全屏時在800x480解析度的裝置中需要1.5M圖片空間(800*480*4 bytes),這樣就可以在記憶體中緩衝2.5屏的圖片。
運用LruCache向ImageView添加圖片時首先先檢查圖片是否存在,如果在直接更行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; } ... }
如何使用磁碟緩衝?
記憶體緩衝對訪問最近使用的圖片時很高效,但是你不能保證它一直會在緩衝中。像GirdView這樣大資料量的組件很容易充滿記憶體緩衝。你的應用可能會被“來電”打斷,在後台時可能會被殺掉,記憶體緩衝就會失效,一旦使用者重新回到應用中時,你需要重新處理每個圖片。
在這種情況下我們可以運用磁碟緩衝儲存已處理的圖片,當圖片不再記憶體中時,減少重新載入的時間,當然從磁碟載入圖片時要比記憶體中慢,需要在後台線程中做,因為磁碟的讀取時間是未知的。
注意:如果你經常訪問圖片,ContentProvider應該是儲存圖片的好地方,如:Gallery圖片管理應用。
下面是一個簡單的DiskLruCache實現。然而推薦的實現DiskLruCache方案請參考Android4.0中(libcore/luni/src/main/java/libcore/io/DiskLruCache.java)源碼。本文使用的是之前版本中的簡單實現(Quick Search中是另外的實現).
顯示是簡單實現DiskLruCache更新後的例子:
private DiskLruCache mDiskCache; private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Overrideprotected 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,此時,為了給使用者快速、平緩的使用者體驗你可能不想重新載入圖片。
多虧你運行了緩衝技術,緩衝可以通過 setRetainInstance(true))傳遞給新的Activity,在Activity重啟後,你可以通過附著的Fragment重新使用已存在的緩衝,這樣就可以快速載入到ImageView中了。
下面是一個當配置改變時用Fragment重用已有的緩衝的例子:
private LruCache mMemoryCache; @Overrideprotected 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的情況下,強制旋轉螢幕,你會發現從保留記憶體緩衝載入圖片時幾乎沒有滯後