本文转载自 zxw136511485的博客: 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:
参考文章:Android中使用的LruCache。