Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android加載圖片你必須知道的技巧

Android加載圖片你必須知道的技巧

編輯:關於Android編程

學習如何處理和加載Bitmap,顯示在UI上非常的重要。如果你不重視這塊,Bitmap講很快耗盡你的內存資源,最終導致oom內存溢出。

移動設備的內存資源很稀缺,很多時候每個應用只能分配到16MB的內存空間。部分機型可能分配的會更多,但是我們必須保證不超過最大內存的限制。 Bitmaps本身就非常占用資源。比如一個Galaxy Nexus拍一張照片2592x1936分辨率。如果使用ARGB_8888(2.3版本以後默認值)加載bitmap的話,加載這張圖將耗費將近19MB(2592*1936*4 bytes)的內存,直接就超過了很多機器的最大內存。 有時候針對ListView, GridView 和 ViewPager 這種控件,我們會有顯示很多圖片的需求,也要為即將可能顯示在屏幕上的圖片做處理,讓圖片為顯示做好准備。

加載大圖片

圖片什麼大小和形狀都有。經常圖片比你的UI控件大得多。例如攝像頭拍出來的照片比你手機屏幕的分辨率高得多。

獲取Bitmap大小和類型

BitmapFactory提供了很多通過各種各樣渠道解碼的方法(decodeByteArray(), decodeFile(), decodeResource(), 等等.) 。這些方法都很容易引起oom內存溢出。每個方法都可以通過BitmapFactory.Options這個類去指定解碼選項。把inJustDecodeBounds設定成true,然後進行解碼,這個時候就不會真的去分配內存,而是返回空的bitmap同時也會返回outWidth, outHeight 和 outMimeType.有了這三個值,我們就可以按照需要對圖片進行壓縮。

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;

除非你對圖片的來源有絕對的信心,不然建議每次解碼都需要檢查圖片的大小和類型。

按比例壓縮後加載

你需要注意這些問題
- 預估加載整個圖片需要多少內存
- 願意從整個應用中,分配多少內存給這張圖
- 目標UI控件比如ImageView 的大小
- 當前設備的屏幕分辨率和尺寸

例如把一個1024x768的圖片全部加載顯示在一個128x96像素的ImageView.上是沒有價值的。
下面是根據目標控件傳入的寬和高,計算出合適的inSampleSize值的方法。

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) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

使用這個方法,第一次解碼設置inJustDecodeBounds為true,得到合適的inSampleSize後,設置inJustDecodeBounds為false,然後第二次解碼。

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線程上處理圖片

如果圖片來源在磁盤或者網絡上(或者其他內存之外的媒介),上面討論的BitmapFactory.decode*方法不應該在主UI線程上執行。加載圖片需要的時間是不可預知的,依賴於很多因素(磁盤讀取速度,網速,圖片大小,CPU速度等)。任何一個工作都可以阻塞UI線程,造成無響應。下面說說用子線程加載bitmaps的問題。

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

這段代碼很簡單,但是可以看到好的代碼習慣,比如使用軟引用避免AsyncTask的引用導致內存洩漏。判斷是否為空,避免空指針異常。使用代碼如下:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

並發的使用

如果我們使用ListView 和 GridView這種重復利用子控件的UI控件時,如果我們使用上面的AsyncTask,當任務完成的時候 無法保證該子控件是否已經被重用了。此外,任務開始的順序和完成的順序也無法保證。
下面是解決方案,創建一個專門的 Drawable 子類來儲存載入圖片的任務引用。這樣使用BitmapDrawable,當任務完成的時候placeholder 中的圖片就能在ImageView顯示了。

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

cancelPotentialWork方法用來檢查是否已經有任務綁定到,如果已經有一個任務了就嘗試取消這個任務(調用 cancel()方法)。
在少數情況下,如果新的任務和已經存在任務的數據一樣,則不需要特殊額外的處理。下面是 cancelPotentialWork 方法的一種實現:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // If bitmapData is not yet set or it differs from the new data
        if (bitmapData == 0 || 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;
}

方法 getBitmapWorkerTask(),獲取和 ImageView關聯的任務:

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() 。
檢查任務是否取消了,和當前的任務和 ImageView引用的任務是否為同一個任務。

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

這套實現適用於ListView 和 GridView ,也適用於其他有子控件回收機制的控件。簡單的在設置ImageView圖片的地方調用loadBitmap 方法就可以。比如在GridView的getView方法中調用loadBitmap。


緩存Bitmaps

加載一張圖片很簡單,加載一大堆圖片就麻煩了,比如 ListView, GridView 或者 ViewPager。LruCache 是官方推薦的緩存圖片的類,低版本可以使用v4支持包。

你的設備可以為每個應用程序分配多大的內存? 設備屏幕上一次最多能顯示多少張圖片?有多少圖片需要進行預加載,因為有可能很快也會顯示在屏幕上? 你的設備的屏幕大小和分辨率分別是多少?一個超高分辨率的設備(例如 Galaxy Nexus) 比起一個較低分辨率的設備(例如 Nexus S),在持有相同數量圖片的時候,需要更大的緩存空間。 圖片的尺寸和大小,還有每張圖片會占據多少內存空間。 圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應該讓一些圖片常駐在內存當中,或者使用多個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. 使用最大可以內存的八分之一作為LruCache的緩存大小
    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 這個是必須實現的,返回的是每個bitmap的大小,每次添加bitmap時都會調用
            // 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);
}

當加載bitmap到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 LruCache很容易超出內存限制, 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);
}

當圖片加載完畢,圖片應該同時緩存到memory和磁盤中,以便以後加載。

處理配置變化

當配置發生變化,例如橫豎屏切換的時候,安卓會銷毀使用新的配置重建activity。為了避免圖片重新處理,我們需要對代碼做一些修改。

這是一個Fragment中保留LruCache不因為配置變化而重新加載的例子。

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

使用第三方圖片加載庫

處理圖片是每個開發者都無法避免的大問題,幸運的是已經有很多非常成熟的第三方圖片加載庫。筆者用過市面上主流的四種加載庫。ImageLoader,Picasso,Glide,Fresco,甚至Volley的圖片加載也嘗試過。每個加載庫各有千秋,可以根據需求做選擇,大大提升開發速度,減少oom異常的概率。筆者更推薦來自facebook的Fresco,在使用過程中,漸進式顯示,三級內存在低端機上體驗更好。送上github上的鏈接: https://github.com/facebook/fresco 。中文文檔非常詳盡,地址: http://fresco-cn.org/ 。開發者可以根據自己的需求,選擇適合自己項目的庫。

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