在Android應用開發中不可避免的會用到圖形映像,這樣就會產生Bitmap對象。如果在開發過程中沒有處理好Bitmap對象就很容易產生Out Of Memory(OOM)的異常。以下列舉幾點使用Bitmap對象需要注意的地方:
- 一個Android應用程式最多隻能使用16M的記憶體,在Android的 Android
Compatibility Definition Document (CDD) 3.7節中描述了不同螢幕解析度及密度的裝置在VM中會分配的大小。
Screen Size |
Screen Density |
Application Memory |
small / normal / large |
ldpi / mdpi |
16MB |
small / normal / large |
tvdpi / hdpi |
32MB |
small / normal / large |
xhdpi |
64MB |
xlarge |
mdpi |
32MB |
xlarge |
tvdpi / hdpi |
64MB |
xlarge |
xhdpi |
128MB |
- Bitmap對象比較佔用記憶體,特別像一些照片。比如使用Google Nexus照一張解析度為2592x1936的照片大概為5M,如果採用ARGB_8888的色彩格式(2.3之後預設使用該格式)載入這個圖片就要佔用19M記憶體(2592*1936*4
bytes),這樣會導致某些裝置直接掛掉。
- Android中很多控制項比如ListView/GridView/ViewPaper通常都會包含很多圖片,特別是快速滑動的時候可能載入大量的圖片,因此圖片處理顯得尤為重要。
下面會從四個方向講述如何最佳化Bitmap的顯示:
- 最佳化大圖片 -- 注意Bitmap處理技巧,使其不會超過記憶體最大限值
通常情況下我們的UI並不需要很精緻的圖片。例如我們使用Gallery顯示照相機拍攝的照片時,你的裝置解析度通常小於照片的解析度。 BitmapFactory類提供了幾個解碼圖片的方法(decodeByteArray(),decodeFile(),decodeResource()等),它們都可以通過BitmapFactory.Options指定解碼選項。設定inJustDecodeBounds屬性為true時解碼並不會產生Bitmap對象,而是返回圖片的解碼資訊(圖片解析度及類型:outWidth,outHeight,outMimeType)然後通過解析度可以算出縮放值,再將inJustDecodeBounds設定為false,傳入縮放值縮放圖片,值得注意的是inJustDecodeBounds可能小於0,需要做判斷。
BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(getResources(), R.id.myimage, options);int imageHeight = options.outHeight;int imageWidth = options.outWidth;String imageType = options.outMimeType;
現在我們知道了圖片的密度,在BitmapFactory.Options中設定inSampleSize值可以縮小圖片。比如我們設定inSampleSize = 4,就會產生一個1/4長*1/4寬=1/16原始圖的圖片。當inSampleSize
< 1的時候預設為1,系統提供了一個calculateInSampleSize()方法來幫我們算這個值:
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { if (width > height) { inSampleSize = Math.round((float)height / (float)reqHeight); } else { inSampleSize = Math.round((float)width / (float)reqWidth); } } return inSampleSize;}
建立一個完整的縮圖方法:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options);}
我們把它設進ImageView中:
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
- 不要在UI線程中處理Bitmap -- 圖片下載/調整大小等不要放在UI線程中處理,可以使用AsyncTask處理並發的問題。
剛剛我們提到過BitmapFactory.decode*的方法,值得注意的是這些方法都不能在UI線程中執行,因為他們的載入過程都是不可靠的,很可能引起應用程式的ANR。
如何解決這個問題呢?我們需要用到AsyncTask來處理並發。AsyncTask提供了一種簡單的方法在後台線程中執行一些操作並反饋結果給UI線程。下面我們來看一個例子:
class BitmapWorkerTask extends AsyncTask { private final WeakReference imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } }}
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId);}
當我們在ListView和GridView中使用AsyncTask的時候會引發一些問題,例如ListView快速滑動的時候其child view是迴圈未被回收的,我們也並不知道AsyncTask什麼時候會完成,有可能AsyncTask還沒執行完之前childView就已經被回收了,下面我們講一種方法可以避免這種情況:
建立一個Drawable的子類來引用儲存工作任務執行後返回的圖片
static class AsyncDrawable extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); }}
在執行BitmapWorkerTask之前,建立一個AsyncDrawable來繫結目標的ImageView:
public void loadBitmap(int resId, ImageView imageView) { if (cancelPotentialWork(resId, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); }}
在給ImageView賦值之前會調用cancelPotentialWork方法,它會使用cancel()方法嘗試取消已經到期的任務。
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; if (bitmapData != data) { // Cancel previous task bitmapWorkerTask.cancel(true); } else { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was cancelled return true;}
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null;}
最後一步,修改BitmapWorkerTask中的onPostExecute()方法
class BitmapWorkerTask extends AsyncTask { ... @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } }}
- 緩衝Bitmap -- 使用緩衝可以改善圖片載入速度提升使用者體驗
- 使用記憶體的Cache
從Android3.1開始,Google提供了一個緩衝類叫LruCache,在此之前我們實現緩衝通常都是用軟引用或是弱引用,但是Google並不建議我們這樣做,因為從Android2.3之後增加了GC回收的頻率。
我們在使用LruCache的時候需要為它設定一個緩衝大小,設定小了緩衝沒有作用,設定大了同樣會導致OOM,因此設定緩衝大小是一門技術活。
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);}
這樣當我們在ImageView中使用Bitmap的時候就可以先從緩衝中擷取,如果緩衝沒有就從網路中擷取:
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.使用硬碟的Cache
我們會使用DiskLruCache來實現硬碟Cache
private DiskLruCache mDiskCache;private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MBprivate 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);}
值得一提的是Android API中並沒有提供DiskLruCache介面,需要自己從4.x源碼中移植至應用程式。源碼地址:
libcore/luni/src/main/java/libcore/io/DiskLruCache.java
3.有時候在處理橫豎屏切換的時候對象會全部重載,這樣緩衝就丟失了。為了避免這個問題,我們除了在Manifest中設定橫豎屏不更新之外,就是使用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); }}
原文:http://developer.android.com/training/displaying-bitmaps/index.html