Android進階練習 - 高效顯示Bitmap(緩存 Bitmaps)


緩存 Bitmaps


     加載單獨的一張圖片用於顯示是很簡單的,但是如果一次性加載大量的圖片時,事情就變得比較複雜了,在許多的情況下(像 ListViewGridView or ViewPager 之類的組件 ),在屏幕上顯示的圖片加上所有待顯示的圖片有可能馬上就會在屏幕上無限制的進行滾動、切換。

      像ListViewGridView 這類組件,它們的子項當不可見時,所佔用的內存會被回收以供正在前臺顯示子項使用。垃圾回收器也會釋放你已經加載了的圖片佔用的內存,假設它們不是一個生命週期很長的對象。這些對內存的有效利用都很好,但如果你想讓你的UI運行流暢的話,就不應該每次顯示時都去重新加載圖片。這個時候保持一些內存和文件緩存就很有必要了,這樣可以讓你快速的重新加載處理圖片。

使用內存緩存


     內存緩存是預先消耗應用的一點內存來存儲數據,以便可以快速的爲應用中的組件提供數據,是一種典型的以空間換時間的策略。LruCache  類(Android v4 Support Library 類庫中開始提供)非常適合來做圖片緩存任務 ,它可以使用一個LinkedHashMap  的強引用來保存最近使用的對象,並且當它保存的對象佔用的內存總和超出了爲它設計的最大內存時會把不經常使用的對象成員踢出以供垃圾回收器回收
Note : 在以前,一個非常流行的內存緩存的實現是使用SoftReference or WeakReference ,但是這種辦法現在並不推薦。從Android 2.3開始,垃圾回收器會更加積極的去回收軟引用和弱引用引用的對象,這樣導致這種做法相當的無效。另外,在Android 3.0之前,圖片數據保存在本地內存中,它們不是以一種可預見的方式來釋放的,這樣可能會導致應用內存的消耗量出現短暫的超限,應用程序崩潰    

     爲了爲 LruCache 設置一個合適的內存大小,有很多因素要進行考慮,如下
          
           一、還剩餘多少內存給你的activity和/或應用使用
              二、屏幕上一次性需要顯示多少張圖片,有多少圖片在等待在屏幕上顯示
              三、手機的大小和密度是多少,一個超高密度屏幕的設備(Galaxy Nexus )往往需要一個更大的緩存
              四、圖片的尺寸和配置是多少,決定了每張圖片佔用內存的大小
              五、圖片的訪問頻率是多少,是否一些比另外一些訪問的更加頻繁。如果是這樣的話,你可能需要在內存中一直保存那幾項,
                    甚至你需要爲不同用途的圖片組配置不同的LruCache  對象 
              六、有些時候你還要去平衡圖片的質量和數量,保存大量低質量的圖片,而另外臨時的去加載一個高質量的圖片版本是很有用的
     
     這裏沒有一個固定的大小或公式來適用所有的應用,你需要詳細的去分析你的應用圖片使用的情況從而來找到一個合適的解決辦法

     一個設置 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.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    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.
            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);
}
 Note:在這個小例子程序中,爲LruCache 設置了一個緩存大小,爲應用程序最大能使用內存的1/8,在一箇中密度或高密度的設備上,緩存的大小最少爲   4M(32/8),以GridView 爲例,如果GridView 滿屏顯示圖片的話,大概會消耗1.5M(800*480*4bytes)內存,所以我們能夠至少緩存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);
    }
}
 
     在圖片加載的Task中,需要把加載好的圖片加入到內存緩存中
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;
    }
    ...
}

使用磁盤文件緩存


     內存緩存能夠快速的獲取到最近顯示的圖片,但不能依賴於它一定能夠獲取到。像 GridView  之類的組件會有一個很大的數據集,很簡單的就能夠把內存緩存填滿。你的應用也有可能被其它的任務(像來電)打斷,你的應用會被切入到後臺,這樣就有可能會被殺死,內存緩存對象也會被銷燬。 當你的應用重新回到前臺顯示時,你的應用又需要一張一張的去加載圖片了。

     磁盤文件緩存能夠用來處理這些情況,保存處理好的圖片,當內存緩存不可用的時候,直接讀取在硬盤中保存好的圖片,這樣可以有效的減少圖片加載的次數。讀取磁盤文件要比直接從內存緩存中讀取要慢一些,而且需要在一個UI主線程外的線程中進行,因爲磁盤的讀取速度是不能夠保證的。磁盤文件緩存顯然也是一種以空間換時間的策略。
Note: 如果圖片使用非常頻繁的話,一個 ContentProvider 可能更適合代替去存儲緩存圖片,比如圖片gallery 應用

     下面是一個使用磁盤文件緩存的程序片段     

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.
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);
}
Note:即使是初始化磁盤緩存都需要進行磁盤操作,因此你不能在UI主線程中進行這項操作,這就意味着可以在磁盤緩存初始化之前進行訪問。爲了避免這種情況的發生,上面的程序片段中,一個鎖對象確保了磁盤緩存沒有初始化完成之前不能夠對磁盤緩存進行訪問
 
     內存緩存在UI線程中進行檢測,磁盤緩存在UI主線程外的線程中進行檢測,當圖片處理完成之後,分別存儲到內存緩存和磁盤緩存中

處理設備配置改變


     應用在運行的時候設備的配置參數有可能改變,例如設備朝向改變,會導致Android銷燬你的Activity然後按照新的配置重啓,這種情況下,你可能會想避免重新去加載處理所有的圖片,讓用戶能有一個流暢的體驗   

     幸運的是,在上一節你已經爲圖片提供了一個非常好的內存緩存。使用Fragment 能夠把內存緩存對象傳遞到新的activity實例中,調用setRetainInstance(true)) 方法來保留Fragment實例。當activity重新創建好後, 被保留的Fragment依附於activity而存在,通過Fragment你可以獲取到已經存在的內存緩存對象了,這樣就可以快速的獲取到圖片,並設置到ImageView上,給用戶一個流暢的體驗。

     下面是一個示例程序片段
private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment mRetainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = RetainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        mRetainFragment.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();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 使得Fragment在Activity銷燬後還能夠保留下來
        setRetainInstance(true);
    }
}
 你可以通過改變設備的朝向來測試上面的程序,你會發現基本上沒有延遲,圖片很快的就顯示在屏幕上。    







          
     
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章