让App中增加LruCache缓存,轻松解决图片过多造成的OOM

炒沙作縻终不饱,缕冰文章费工巧。这篇文章主要讲述让App中增加LruCache缓存,轻松解决图片过多造成的OOM相关的知识,希望能为你提供帮助。
上次有过电话面试中问到android中的缓存策略,当时模糊不清的回答,如今好好理一下吧。

Android中普通情况下採取的缓存策略是使用二级缓存。即内存缓存+硬盘缓存—> LruCache+DiskLruCache。二级缓存能够满足大部分的需求了,另外还有个三级缓存(内存缓存+硬盘缓存+网络缓存),当中DiskLruCache就是硬盘缓存,下篇再讲吧!
1、那么LruCache究竟是什么呢?查了下官方资料。是这样定义的:
LruCache 是对限定数量的缓存对象持有强引用的缓存,每一次缓存对象被訪问,都会被移动到队列的头部。当有对象要被加入到已经达到数量上限的 LruCache 中,队列尾部的对象将会被移除,并且可能会被垃圾回收器回收。LruCache?中的 Lru 指的是“Least Recently Used-最近最少使用算法”。
这就意味着,LruCache 是一个能够推断哪个缓存对象是最近最少使用的缓存对象。从而把最少使用的移至队尾,而最近使用的则留在队列前面了。举个样例:比方我们有a、b、c、d、e五个元素,而a、c、d、e都被訪问过。唯有b元素没有訪问,则b元素就成为了最近最少使用元素了,就移至在队尾了。
从上面的叙述中我们总结能够知道LruCache核心思想就两点:
1、LruCache使用的是最近最少使用算法。最近使用最少的将会移至到队尾。而最近刚刚使用的则会移至到头部。

2、LruCache缓存的大小是限定的(限定的大小由我们定义),当LruCache存储空间满了就会移除队尾的而为新的对象的加入腾出控件。
2、我们还是从源代码入手学学LruCache究竟怎么使用吧:我们先看看LruCache类中的变量有什么:

让App中增加LruCache缓存,轻松解决图片过多造成的OOM

文章图片


显示发现一堆int类型的变量。另一个最重要的LinkedHashMap< K,V> 这个队列,通俗的讲LinkedHashMap< K,V> 就是一个双向链表存储结构。
各个变量的意思为:
size - LruCache中已经存储的大小
maxSize - 我们定义的LruCache缓存最大的空间
putCount - put的次数(为LruCache加入缓存对象的次数)
createCount - create的次数
evictionCount - 回收的次数
hitCount - 命中的次数
missCount - 丢失的次数
再看看构造器:

public LruCache(int maxSize) { if (maxSize < = 0) { throw new IllegalArgumentException("maxSize < = 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap< K, V> (0, 0.75f, true); }

发现须要传入一个int类型的值。顾名思义。这就是我们定义的LruCache缓存的空间大小了,普通情况下我们能够得到应用程序的最大可用空间,然后按百分比取值设置给它就可以。
再看看其他一些比較重要的方法:
put()方法:

public final V put(K key, V value) { if (key == null || value =https://www.songbingjia.com/android/= null) { throw new NullPointerException("key == null || value =https://www.songbingjia.com/android/= null"); }V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } }if (previous != null) { entryRemoved(false, key, previous, value); }trimToSize(maxSize); return previous; }

通过该方法我们能够知道LruCache中是通过< Key,Value> 形式存储缓存数据的。
意思就是我们把一个Value存储到LruCache中,并设置相应键值为key。然后推断key和value都不能为空,否则就抛异常了。之后把该Value移至队列的头部。
get()方法:

public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); }V mapValue; synchronized (this) { mapValue = https://www.songbingjia.com/android/map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; }/* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. */V createdValue = create(key); if (createdValue == null) { return null; }synchronized (this) { createCount++; mapValue = map.put(key, createdValue); if (mapValue != null) { // There was a conflict so undo that last put map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } }if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } }

该方法就是得到相应key缓存的Value,假如该Value存在,返回Value并且移至该Value至队列的头部,这也证实了最近最先使用的将会移至队列的头部。
假如Value不存在则返回null。
remove()方法:

public final V remove(K key) { if (key == null) { throw new NullPointerException("key == null"); }V previous; synchronized (this) { previous = map.remove(key); if (previous != null) { size -= safeSizeOf(key, previous); } }if (previous != null) { entryRemoved(false, key, previous, null); }return previous; }

该方法就是从LruCache缓存中移除相应key的Value值。
sizeof()方法:一般须要重写的:

protected int sizeOf(K key, V value) { return 1; }

重写它计算不同的Value的大小。
一般我们会这样重写:

mLruCache = new LruCache< String, Bitmap> (cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { if(bitmap!=null){ return bitmap.getByteCount(); } return 0; } };


好了,总结一下使用LruCache的原理:比方像ImageView中载入一张图片时候,首先会在LruCache的缓存中检查是否有相应的key值(get( key)),假设有就返回相应的Bitmap。从而更新ImageView。假设没有则又一次开启一个异步线程来又一次载入这张图片。
来看看用LruCache缓存Bitmap的样例:

public class MyLruCache extends AppCompatActivity{ private LruCache< String,Bitmap> mLruCache; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //得到应用程序最大可用内存 int maxCache = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxCache / 8; //设置图片缓存大小为应用程序总内存的1/8 mLruCache = new LruCache< String, Bitmap> (cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { if(bitmap!=null){ return bitmap.getByteCount(); } return 0; } }; } /** * 加入Bitmap到LruCache中 * * @param key * @param bitmap */ public void putBitmapToLruCache(String key, Bitmap bitmap) { if (getBitmapFromLruCache(key) == null) { mLruCache.put(key, bitmap); } }/** * @param key * @return 从LruCache缓存中获取一张Bitmap。没有则会返回null */ public Bitmap getBitmapFromLruCache(String key) { return mLruCache.get(key); } }

以下通过一个实例来看看怎么用:
先看看效果吧:
让App中增加LruCache缓存,轻松解决图片过多造成的OOM

文章图片


让App中增加LruCache缓存,轻松解决图片过多造成的OOM

文章图片


贴下主要代码:
MainActivity:

public class MainActivity extends ActionBarActivity { private GridView mGridView; private List< String> datas; private Toolbar mToolbar; private GridViewAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.v("zxy", "cache:" + getCacheDir().getPath()); Log.v("zxy", "Excache:" + getExternalCacheDir().getPath()); mToolbar = (Toolbar) findViewById(R.id.toolbar); mToolbar.setTitleTextColor(Color.WHITE); mToolbar.setNavigationIcon(R.mipmap.icon); setSupportActionBar(mToolbar); initDatas(); mGridView = (GridView) findViewById(R.id.gridView); mAdapter = new GridViewAdapter(this, mGridView, datas); mGridView.setAdapter(mAdapter); mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView< ?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "position=" + position + ",id=" + id, Toast.LENGTH_SHORT).show(); } }); }public void initDatas() { datas = new ArrayList< > (); for (int i = 0; i < 55; i++) { datas.add(URLDatasTools.imageUrls[i]); } }@Override protected void onDestroy() { super.onDestroy(); mAdapter.cancelAllDownloadTask(); //取消全部下载任务 } }



GridViewAdapter:

public class GridViewAdapter extends BaseAdapter implements AbsListView.OnScrollListener { private List< DownloadTask> mDownloadTaskList; //全部下载异步线程的集合 private Context mContext; private GridView mGridView; private List< String> datas; private LruCache< String, Bitmap> mLruCache; private int mFirstVisibleItem; //当前页显示的第一个item的位置position private int mVisibleItemCount; //当前页共显示了多少个item private boolean isFirstRunning = true; public GridViewAdapter(Context context, GridView mGridView, List< String> datas) { this.mContext = context; this.datas = datas; this.mGridView = mGridView; this.mGridView.setOnScrollListener(this); mDownloadTaskList = new ArrayList< > (); initCache(); }private void initCache() { //得到应用程序最大可用内存 int maxCache = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxCache / 8; //设置图片缓存大小为应用程序总内存的1/8 mLruCache = new LruCache< String, Bitmap> (cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { if (bitmap != null) { return bitmap.getByteCount(); } return 0; } }; }@Override public int getCount() { return datas.size(); }@Override public Object getItem(int position) { return datas.get(position); }@Override public long getItemId(int position) { return position; }@Override public View getView(int position, View convertView, ViewGroup parent) { convertView = LayoutInflater.from(mContext).inflate(R.layout.layout_item, parent, false); ImageView mImageView = (ImageView) convertView.findViewById(R.id.imageView); TextView mTextView = (TextView) convertView.findViewById(R.id.textView); String url = datas.get(position); mImageView.setTag(String2MD5Tools.hashKeyForDisk(url)); //设置一个Tag为md5(url),保证图片不错乱显示 mTextView.setText("第" + position + "项"); setImageViewForBitmap(mImageView, url); return convertView; }/** * 给ImageView设置Bitmap * * @param imageView * @param url */ private void setImageViewForBitmap(ImageView imageView, String url) { String key = String2MD5Tools.hashKeyForDisk(url); //对url进行md5编码 Bitmap bitmap = getBitmapFromLruCache(key); if (bitmap != null) { //假设缓存中存在。那么就设置缓存中的bitmap imageView.setImageBitmap(bitmap); } else { //不存在就设置个默认的背景色 imageView.setBackgroundResource(R.color.color_five); } }/** * 加入Bitmap到LruCache中 * * @param key * @param bitmap */ public void putBitmapToLruCache(String key, Bitmap bitmap) { if (getBitmapFromLruCache(key) == null) { mLruCache.put(key, bitmap); } }/** * @param key * @return 从LruCache缓存中获取一张Bitmap,没有则会返回null */ public Bitmap getBitmapFromLruCache(String key) { return mLruCache.get(key); }@Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == SCROLL_STATE_IDLE) {//GridView为精巧状态时,让它去下载图片 loadBitmap(mFirstVisibleItem, mVisibleItemCount); } else { //滚动时候取消全部下载任务 cancelAllDownloadTask(); } }@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { mFirstVisibleItem = firstVisibleItem; mVisibleItemCount = visibleItemCount; if (isFirstRunning & & visibleItemCount > 0) {//首次进入时载入图片 loadBitmap(mFirstVisibleItem, mVisibleItemCount); isFirstRunning = false; } }/** * 载入图片到ImageView中 * * @param mFirstVisibleItem * @param mVisibleItemCount */ private void loadBitmap(int mFirstVisibleItem, int mVisibleItemCount) { //首先推断图片在不在缓存中,假设不在就开启异步线程去下载该图片 for (int i = mFirstVisibleItem; i < mFirstVisibleItem + mVisibleItemCount; i++) { final String url = datas.get(i); String key = String2MD5Tools.hashKeyForDisk(url); Bitmap bitmap = getBitmapFromLruCache(key); if (bitmap != null) { //缓存中存在该图片的话就设置给ImageView ImageView mImageView = (ImageView) mGridView.findViewWithTag(String2MD5Tools.hashKeyForDisk(url)); if (mImageView != null) { mImageView.setImageBitmap(bitmap); } } else { //不存在的话就开启一个异步线程去下载 DownloadTask task = new DownloadTask(); mDownloadTaskList.add(task); //把下载任务加入至下载集合中 task.execute(url); } } }class DownloadTask extends AsyncTask< String, Void, Bitmap> { String url; @Override protected Bitmap doInBackground(String... params) { //在后台開始下载图片 url = params[0]; Bitmap bitmap = downloadBitmap(url); if (bitmap != null) { //把下载好的图片放入LruCache中 String key = String2MD5Tools.hashKeyForDisk(url); putBitmapToLruCache(key, bitmap); } return bitmap; }@Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); //把下载好的图片显示出来 ImageView mImageView = (ImageView) mGridView.findViewWithTag(String2MD5Tools.hashKeyForDisk(url)); if (mImageView != null & & bitmap != null) { mImageView.setImageBitmap(bitmap); mDownloadTaskList.remove(this); //把下载好的任务移除 } }}/** * @param tasks * 取消全部的下载任务 */ public void cancelAllDownloadTask(){ if(mDownloadTaskList!=null){ for (int i = 0; i < mDownloadTaskList.size(); i++) { mDownloadTaskList.get(i).cancel(true); } } } /** * 建立网络链接下载图片 * * @param urlStr * @return */ private Bitmap downloadBitmap(String urlStr) { HttpURLConnection connection = null; Bitmap bitmap = null; try { URL url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); connection.setDoInput(true); connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { InputStream mInputStream = connection.getInputStream(); bitmap = BitmapFactory.decodeStream(mInputStream); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (connection != null) { connection.disconnect(); } } return bitmap; } }


好了。LruCache就介绍到这了。当中上面的样例有个不足就是没有做图片大小检查,过大的图片没有压缩。。


下一篇来介绍下怎么对过大的图片进行压缩!!!
【让App中增加LruCache缓存,轻松解决图片过多造成的OOM】源代码地址:http://download.csdn.net/detail/u010687392/8920169










    推荐阅读