1. 程式人生 > >Android 快取淺談(一) LruCache

Android 快取淺談(一) LruCache

    Android應用開發好多場景都是手機和web伺服器之間進行通訊,從服務端需要獲取資料,但是當訪問的資料比較大,比較多,並且是重複資料時,會極大影響效能,甚至應用崩潰,手機卡死,這時候就要考慮快取機制了!Android中可通過快取來減少頻繁的網路操作,減少流量、提升效能。

    在實際開發中,快取機制使用最頻繁的便是圖片快取!目前大部分的App都是圖文結合,從web伺服器獲取文字和圖片,文字顯示很快,圖片基本上是先下載到手機本地,然後再顯示,如果圖片很多、很大,每次載入同一張圖片,都去網路下載,那麼App渲染的速度是比較慢的,這樣的體驗很差!所以,類似這樣的場景,便要使用快取機制!

     目前快取機制使用大致流程是,當App需要載入某一張圖片時,先去手機記憶體中去找該圖片,如果有,那麼直接顯示,如果無,則去手機sd卡或者手機外部儲存中找該圖片,如果有,那麼直接顯示,如果無,那麼此時才去網路下載該圖片。這種機制常稱為三級快取策略。

      三級快取策略,首先從記憶體中載入圖片,因為從記憶體中獲取圖片速度是最快的,但是由於記憶體的有限的,所以快取的圖片也是有限的!所以從記憶體快取使用LRUcache。外部快取即磁碟快取,相比記憶體快取而言速度要來得慢很多,但容量很大,這裡的使用的是DiskLruCache。

        Android的快取機制是基於Java的快取機制。Java的快取機制有四種,強引用、軟引用、弱引用和虛引用。著重看看軟引用(SoftReference)和弱引用(WeakReference)。

1.  軟引用(SoftReference)。

    如果一個物件具有軟引用,記憶體空間足夠,垃 圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的高 速快取。使用軟引用能防止記憶體洩露,增強程式的健壯性。

2. 弱引用(WeakReference)。

    如果一個物件只具有弱引用,那麼在垃圾回收器執行緒掃描的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。弱引用也可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。

    兩者的區別 

弱引用與軟引用的根本區別在於:只具有弱引用的物件擁有更短暫的生命週期,可能隨時被回收。而只具有軟引用的物件只有當記憶體不夠的時候才被回收,在記憶體足夠的時候,通常不被回收。

   以上的描述是從Java方面給出的,但是在2.3版本後,Google不建議使用這兩種快取機制!那麼看看Google官網具體是如何描述的,

原文,

	Note: In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache, 
	however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive 
	with collecting soft/weak references which makes them fairly ineffective. In addition, prior to Android 3.0 (API Level 11), 
	the backing data of a bitmap was stored in native memory which is not released in a predictable manner, potentially causing 
	an application to briefly exceed its memory limits and crash.
翻譯,
	Note: 在過去,一種比較流行的記憶體快取實現方法是使用軟引用(SoftReference)或弱引用(WeakReference)對Bitmap進行快取,
	然而我們並不推薦這樣的做法。從Android 2.3 (API Level 9)開始,垃圾回收機制變得更加頻繁,這使得釋放軟(弱)引用的頻率也
	隨之增高,導致使用引用的效率降低很多。而且在Android 3.0 (API Level 11)之前,備份的Bitmap會存放在Native Memory中,
	它不是以可預知的方式被釋放的,這樣可能導致程式超出它的記憶體限制而崩潰。

     Google 建議我們使用強引用(strong referenced)。

原文,

	A memory cache offers fast access to bitmaps at the cost of taking up valuable application memory. 
	The LruCache class (also available in the Support Library for use back to API Level 4) is particularly 
	well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced 
	LinkedHashMap and evicting the least recently used member before the cache exceeds its designated size.
翻譯,
	記憶體快取以花費寶貴的程式記憶體為前提來快速訪問點陣圖。LruCache類(在API Level 4的Support Library中也可以找到)
	特別適合用來快取Bitmaps,它使用一個強引用(strong referenced)的LinkedHashMap儲存最近引用的物件,
	並且在快取超出設定大小的時候剔除(evict)最近最少使用到的物件。

並且在API level 12中已經引入了LruCache類。接下來,就看看LruCache的介紹以及使用。LruCache便是上面提到的記憶體快取策略。

一、LruCache介紹。

1. LRU。

LRU是Least Recently Used 近期最少使用演算法。記憶體管理的一種頁面置換演算法,對於在記憶體中但又不用的資料塊(記憶體塊)叫做LRU,作業系統會根據哪些資料屬於LRU而將其移出記憶體而騰出空間來載入另外的資料。什麼是LRU演算法? LRU即最近最少使用,常用於頁面置換演算法,是為虛擬頁式儲存管理服務的。

2. LruCache。

LruCache這個類在android.util包下,是API level 12引入的,對於API level 12之前的系統可以使用support library中的LruCache。這個類非常適合用來快取圖片,它的主要演算法原理是把最近使用的物件用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的物件在快取值達到預設定值之前從記憶體中移除。

二、使用。

要實現LruCache快取策略的步驟有:

 (1).要先設定快取圖片的記憶體大小,基本上設定為手機記憶體的1/8,
           手機記憶體的獲取方式:int MAXMEMONRY = (int) (Runtime.getRuntime() .maxMemory() / 1024);
 (2).LruCache裡面的鍵值對分別是URL和對應的圖片;
 (3).重寫了一個叫做sizeOf的方法,返回的是圖片數量。

下面通過一個例項看看如何使用,實現一個ListView中包含圖片,具體的程式碼例項,

(1). 新建佈局adapter_item_layout.xml,該佈局檔案是ListView的item的佈局,只有一個ImageView控制元件,顯示圖片。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="10dp"
    >
    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="100dp"
        android:layout_height="100dp" />

</LinearLayout>
(2). 新建一個ImageAdapter,繼承自BaseAdapter,具體程式碼如下,
/**
 * 圖片介面卡
 */
public class ImageAdapter extends BaseAdapter implements AbsListView.OnScrollListener {

    private Context mContext;
    private ImageLoader mImageLoader;//圖片處理類,包含圖片快取 下載等
    private int mStart;// 第一張可見圖片的下標 
    private int mEnd;// 最後一張可見圖片的下標 
    public static String[] URLS;//圖片下載路徑集合
    private boolean mFirstIn;//記錄是否剛開啟程式

    public ImageAdapter(Context context, String[] data, ListView listView) {
        URLS = data;
        mContext = context;
        mImageLoader = new ImageLoader(listView);
        mFirstIn = true;
        listView.setOnScrollListener(this);
    }

    @Override
    public int getCount() {
        return URLS.length;
    }

    @Override
    public Object getItem(int position) {
        return URLS[position];
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolader holader = null;
        if (convertView == null) {
            holader = new ViewHolader();
            convertView = LayoutInflater.from(mContext).inflate(R.layout.adapter_item_layout, null);
            convertView.setTag(holader);
        } else {
            holader = (ViewHolader) convertView.getTag();
        }
        holader.iv = (ImageView) convertView.findViewById(R.id.iv_icon);
        holader.iv.setImageResource(R.mipmap.ic_launcher);
        String imageUrl = URLS[position];
        holader.iv.setTag(imageUrl);
        mImageLoader.showImage(holader.iv, imageUrl);
        return convertView;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == SCROLL_STATE_IDLE) {
            //載入可見項
            mImageLoader.showIamges(mStart, mEnd);
        } else {
            // 停止任務
            mImageLoader.cancelAllTask();
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mStart = firstVisibleItem;
        mEnd = firstVisibleItem + visibleItemCount;
        //第一次顯示時候呼叫
        if (mFirstIn && visibleItemCount > 0) {
            mImageLoader.showIamges(mStart, mEnd);
            mFirstIn = false;
        }
    }

    class ViewHolader {
        public ImageView iv;
    }

}
ImageAdapter還實現了AbsListView.OnScrollListener,用於滑動監聽,第一次顯示的時候,直接載入可見項圖片,當滑動時,取消所有下載項,當滑動停止,載入可見項圖片。程式碼比較簡單。下面再看看ImageLoader類,
/**
 * 圖片處理類
 */
public class ImageLoader {

    private LruCache<String, Bitmap> mCache;//LruCache快取物件
    private ListView mListView;// ListView的例項
    private Set<IamgeLoaderTask> mTask;//下載任務的集合
    public ImageLoader(ListView listView) {
        mListView = listView;
        mTask = new HashSet<>();
        //獲取最大可用記憶體
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        //設定快取的大小
        int cacheSize = maxMemory / 8;
        mCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //在每次存入快取的時候呼叫
                return value.getByteCount();
            }
        };
    }

    /**
     * 將bitmap加入到快取中
     *
     * @param url LruCache的鍵,即圖片的下載路徑
     * @param bitmap LruCache的值,即圖片的Bitmap物件
     */
    public void addBitmapToCache(String url, Bitmap bitmap) {
        if (getBitmapByImageUrl(url) == null) {
            mCache.put(url, bitmap);
        }
    }

    /**
     * 從快取中獲取bitmap
     *
     * @param url LruCache的鍵,即圖片的下載路徑
     * @return 對應傳入鍵的Bitmap物件,或者null
     */
    public Bitmap getBitmapFromCache(String url) {
        Bitmap bitmap = mCache.get(url);
        return bitmap;
    }

    /**
     * 載入Bitmap物件。
     *
     * @param start 第一個可見的ImageView的下標
     * @param end   最後一個可見的ImageView的下標
     */
    public void showIamges(int start, int end) {
        for (int i = start; i < end; i++) {
            String imageUrl = ImageAdapter.URLS[i];
            //從快取中取圖片
            Bitmap bitmap = getBitmapFromCache(imageUrl);
            //如果快取中沒有,則去下載
            if (bitmap == null) {
                IamgeLoaderTask task = new IamgeLoaderTask(imageUrl);
                task.execute();
                mTask.add(task);
            } else {
                ImageView imageView = (ImageView) mListView.findViewWithTag(imageUrl);
                imageView.setImageBitmap(bitmap);
            }

        }
    }

    /**
     * 取消所有下載任務
     */
    public void cancelAllTask() {
        if (mTask != null) {
            for (IamgeLoaderTask task : mTask) {
                task.cancel(false);
            }
        }
    }

    /**
     * 顯示圖片
     *
     * @param imageView
     * @param imageUrl
     */
    public void showImage(ImageView imageView, String imageUrl) {
        //從快取中取圖片
        Bitmap bitmap = getBitmapFromCache(imageUrl);
        //如果快取中沒有,則去下載
        if (bitmap == null) {
            imageView.setImageResource(R.mipmap.ic_launcher);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

    /**
     * 下載並顯示圖片
     */
    private class IamgeLoaderTask extends AsyncTask<Void, Void, Bitmap> {
        private String mImageUrl;


        IamgeLoaderTask(String imageUrl) {
            mImageUrl = imageUrl;
        }

        @Override
        protected Bitmap doInBackground(Void... params) {
            Bitmap bitmap = getBitmapByImageUrl(mImageUrl);
            if (bitmap != null) {
                addBitmapToCache(mImageUrl, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTask.remove(this);
        }
    }

    /**
     * 根據圖片路徑下載圖片Bitmap
     *
     * @param imageUrl 圖片網路路徑
     * @return
     */
    public Bitmap getBitmapByImageUrl(String imageUrl) {
        Bitmap bitamp = null;
        HttpURLConnection con = null;
        try {
            URL url = new URL(imageUrl);
            con = (HttpURLConnection) url.openConnection();
            con.setConnectTimeout(10 * 1000);
            con.setReadTimeout(10 * 1000);
            bitamp = BitmapFactory.decodeStream(con.getInputStream());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (con != null) {
                con.disconnect();
            }
        }
        return bitamp;
    }
}
ImageLoader有圖片下載,快取處理。思路是,當要顯示一張圖片時,先去快取中查詢,如果快取中沒有,那麼開啟非同步任務去下載該圖片,下載成功後,顯示並加入到快取中;如果快取中有,則取出直接顯示。

最後在看看MainActivity的程式碼,程式碼如下,

public class MainActivity extends Activity {


    ImageAdapter adapter;


   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView lv = (ListView) findViewById(R.id.lv);
        adapter = new ImageAdapter(MainActivity.this, Images.imageUrls, lv);
        lv.setAdapter(adapter);
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        adapter.mImageLoader.cancelAllTask();
    }
}
還有佈局檔案activity_main.xml,只有一個ListView,
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

   <ListView
       android:id="@+id/lv"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       />

</LinearLayout>
MainActivity中的程式碼非常簡單,就不多說了,在Activity被銷燬時取消掉了所有的下載任務。另外由於我們使用了網路,還需要在AndroidManifest.xml中加入網路許可權的宣告。

執行後,效果如下:


至此,有關LruCache的使用就結束了!下篇文章講解DishLruCache,詳情請看 Android 快取淺談(二) DiskLruCache 。

PS: