1. 程式人生 > >Android有效地展示大圖片(三)

Android有效地展示大圖片(三)

圖片快取

只下載一張圖片在你的UI上時非常簡單的。但是如果你需要一次性下很多圖片就不這麼容易了。在很多情況下(比如ListView,GridView或者是ViewPager),要展示在螢幕上的圖片加上即將要展示的圖片,這個數量可是沒有什麼大小限制的。 以上提到的控制元件,為了減少記憶體消耗,他們都會迴圈利用已經觸屏的子View,如果你對某個Bitmap不再持有引用,那麼java的垃圾回收機制也會把這個bitmap收走。這樣的話,每次一些圖片重新上屏時我們需要重新處理他們。但是為了保證UI的流暢性,我們必須避免他們。這時記憶體快取和硬碟快取就會派上用場了,通過他們我們可以讓這些空間快速的載入處理過的圖片。、

Use a Memory Cache


記憶體快取可以讓我們快速得到bitmap,當然其代價就是佔用可貴的程式記憶體。LruCache類非常適合存放快取圖片,該類把最近用到的物件以強引用存放到LinkedHashMap中去,並在快取大小超過規定之前把最不常用的物件刪除。
在以前,通常用SoftReference或者 WeakReference來作為快取來儲存bitmap.然而如今我們並不推薦這樣做。從Android 2.3 開始,垃圾回收機制對soft/weak references 的攻擊性變強,使得弱/軟 引用失去了效果。另外,在3.0以上,bitmap的相關資訊會被儲存在當地記憶體中,我們也不知道他們什麼時候釋放。這樣的潛在性通常會使得一個程式超出自己的記憶體限制而崩潰。

為了給LruCache一個確定的大小。我們需要考慮以下幾個因素:

你的activity或者是application的剩餘的記憶體有多密集?
螢幕一次性要顯示多少圖片,將要上屏的圖片有幾個?
螢幕大小和解析度是多少?相比於Nexus S(hdpi),Galaxy Nexus(xhdpi)需要更大的記憶體來儲存相同數量的圖片。
這些圖片的尺寸和形狀是怎樣的,它們每個會佔有多少記憶體?
這些圖片被取出的頻繁嗎?是不是一些比另外一些更頻繁。如果是這樣的話,那麼你可能需要儲存那幾個圖片一直在記憶體中。或者你可以多建立幾個LruCache
來分組存放圖片。
均衡圖片的質量和數量。有時候儲存很多的質量低的圖片是很有用處的。而後臺在下載的時候可能會是高質量的圖片。

LruCache的大小並沒有一個特定的值。它的大小取決於你的分析。快取太小隻會引起不必要的開銷而沒有什麼實際用處,太大的話就只會留給程式執行的空間較小,容易造成OOM 。
下面是一個建立LruCache的例子:
private LruCache<String, Bitmap> 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.得到虛擬機器的最大的可用記憶體,並以KB的單位儲存
    //超出此記憶體就會報OOM.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
   //用可用記憶體的1/8作為快取的大小
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.快取的大小以KB為單位。
            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);
}


在這個例子中,程式可用記憶體的1/8分給了快取。在解析度中高階的裝置上,這最低也是4M,在800x480的裝置上,一個充滿圖片的全屏的GridView大約會用1.5M,所以這個快取最少儲存了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<Integer, Void, Bitmap> {
    ...
    // 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;
    }
    ...
}

Use a Disk Cache

記憶體快取在獲取最近瀏覽過的圖片速度的確很快,但是我們不能只依賴只在這些記憶體裡才有的圖片。像是GridView這種需要大量資料填充的控制元件很快就把記憶體快取充滿了。而且你的程式也可能會被其餘的程式打斷,比如一個來電,後臺執行緒可能被銷燬,記憶體快取就會被破壞。一旦resume,程式還得重新處理圖片。 在上述這些情況下,硬碟快取就派上了用場。硬碟快取會在儲存被處理過得圖片至於也減少了重新下載那些在記憶體中找不到的圖片的次數。當然從硬盤獲取圖片的速度相比較而言比較慢,所以應該從後臺執行緒進行。而且硬碟讀取速度也是不可預知的。
注:當獲取圖片比較頻繁時,用ContentProvider是一個不錯的選擇,比如當你寫圖片畫廊應用時。
下面的例項程式碼用到了DiskLruCache的實現類,該類是從Android source拉下來的。下面是除了新增一個記憶體快取外也新增一個硬碟快取的程式碼:
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<File, Void, Void> {
    @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<Integer, Void, Bitmap> {
    ...
    // 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.
//在目標app的快取目錄下建立唯一的子目錄。如果可以用sdcard上的則用sdcard 的空間,如果沒有則用手機本身的儲存。
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);
}

注:初始化硬碟記憶體不可以在主執行緒執行。但是在硬碟快取完成初始化之前我們也不可以訪問該快取。為了達到這個目的,上述程式碼中用了一個鎖物件來控制,這樣的話只有等硬碟初始化完成,app才有機會訪問硬碟快取。
檢查記憶體快取中是否有目標圖是在主執行緒中進行的,而檢查硬碟快取則是在後臺執行緒進行的。當圖片處理完畢後,最終的bitmap將在兩個快取中都進行儲存以便將來使用。

Handle Configuration Changes

執行期間會有一些配置上的變化,比如說螢幕切換方向,是的Android重啟當前Activity,而為了讓使用者有更流暢的體驗我們又不想重新處理這些圖片。 幸運的是,在Use a Memory Cache  部分,你有了一個存放bitmap的快取,這個快取可以通過一個Fragment傳遞給新的Activity。這個fragment是通過呼叫setRetainInstance(true)而得以保留的。當Activity重新建立後,這個一直存在的Fragment便會重新attached,這樣你就可以獲得記憶體快取了,圖片自然會快速載入了。
下面的程式碼就是在切屏時用Fragment來儲存LruCache.
private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> 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和不保留Fragment時分別旋轉螢幕。你會發現,當保留了cache時,切屏時填充圖片有很小或者幾乎沒有停滯。
在快取中找不到的圖片可能放在硬碟快取中。如果硬碟中也沒有,那麼他們就會從後臺處理了。
附非同步載入圖片原始碼下載地址:http://files.cnblogs.com/domyself918/DisplayingBitmaps.zip