Android Bitmap映像最佳化

來源:互聯網
上載者:User

    在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 -- 使用緩衝可以改善圖片載入速度提升使用者體驗
  1. 使用記憶體的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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.