前两篇我们介绍了一般的优化ListView方法以及DiskLruCache优化ListView,见android-----带你一步一步优化ListView(一)和android-----带你一步一步优化ListView(二),这一篇我们将从内存缓存的角度来完成ListView的优化,使用的是LruCache,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除,并没有一个固定的缓存大小是符合所有应用程序的,我们应该根据自己应用程序的状况设置出合理的缓存大小,下面我们通过实例来学习一下LruCache具体怎么使用:
首先定义显示ListView的布局文件:listview.xml
<?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="match_parent"
android:orientation="vertical" >
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
接着定义每个item显示的样式item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
/>
<TextView
android:id="@+id/textView"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_toRightOf="@id/imageView"
android:layout_marginTop="20dp"
android:layout_marginRight="70dp"
/>
</RelativeLayout>
接下来就是最重要的ListViewAdapter适配器内容了,代码有点长度,先贴出来,接下来慢慢解释:
public class ListViewAdapter extends BaseAdapter{
//存储所有将要访问的路径
public List<String> list;
public LruCache<String, Bitmap> lruCache;
public LayoutInflater inflater;
public ListView listView;
public int reqWidth;
public int reqHeight;
public Set<ImageAsyncTask> tasks = new HashSet<ListViewAdapter.ImageAsyncTask>();
public ListViewAdapter(Context context,List<String> list,LruCache<String, Bitmap> lruCache,ListView listView,ImageView imageView)
{
this.list = list;
this.lruCache = lruCache;
this.inflater = LayoutInflater.from(context);
this.listView = listView;
LayoutParams params = imageView.getLayoutParams();
reqWidth = params.width;
reqHeight = params.height;
}
@Override
public int getCount() {
return list.size();
}
@Override
public String getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
ViewHolder holder = null;
if(convertView == null)
{
view = inflater.inflate(R.layout.item, null);
holder = new ViewHolder();
holder.imageView = (ImageView) view.findViewById(R.id.imageView);
holder.textView = (TextView) view.findViewById(R.id.textView);
view.setTag(holder);
}else
{
view = convertView;
holder = (ViewHolder) view.getTag();
}
//为ImageView以及TextView设置Tag防止出现乱序
holder.imageView.setTag(position);
holder.textView.setTag(position+"#");
return view;
}
/**
* 添加当前key值对应的Bitmap到LruCache中
* @param key
* @param bitmap
*/
public void addBitmapToMemoryCache(String key,Bitmap bitmap)
{
if(getBitmapFromMemoryCache(key) == null)
{
lruCache.put(key, bitmap);
}
}
/**
* 从LruCache中获取对应于key的Bitmap对象
* @param key
* @return
*/
public Bitmap getBitmapFromMemoryCache(String key)
{
return lruCache.get(key);
}
/**
* 加载某一位置上的图片
* @param url
* @param position
*/
public void loadImage(int position)
{
//获取到position位置对应的url
String url = getItem(position);
Bitmap bitmap = null;
bitmap = getBitmapFromMemoryCache(url);
if(bitmap != null)
{
//表示缓存中存在对应的于此url的Bitmap,则直接获得该Bitmap并且显示到ListView上面
ImageView imageView = (ImageView) listView.findViewWithTag(position);
TextView textView = (TextView) listView.findViewWithTag(position+"#");
if(imageView != null)
imageView.setImageBitmap(bitmap);
if(textView != null)
textView.setText("从缓存中获取的");
}else
{
//开启线程从网络中加载图片
ImageAsyncTask task = new ImageAsyncTask(listView, position);
task.setOnImageLoadListener(new OnImageLoadListener() {
@Override
public void onSuccessLoad(Bitmap bitmap) {
System.out.println("加载图片成功");
}
@Override
public void onFailureLoad() {
System.out.println("加载图片失败");
}
});
tasks.add(task);
task.execute(url);//开启线程加载图片
}
}
/**
* 暂停所有任务(为了防止在滑动的时候仍然有线程处于请求状态)
*/
public void cancelTask()
{
if(tasks != null)
{
for(ImageAsyncTask task: tasks)
task.cancel(false);//暂停任务
}
}
/**
* 对图片进行压缩处理
* @param in
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeSampleBitmapFromStream(InputStream in,int reqWidth,int reqHeight)
{
//设置BitmapFactory.Options的inJustDecodeBounds属性为true表示禁止为bitmap分配内存
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
byte[] data = inputStreamToByteArray(in);
//这次调用的目的是获取到原始图片的宽、高,但是这次操作是没有写内存操作的
Bitmap beforeBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//设置这次加载图片需要加载到内存中
options.inJustDecodeBounds = false;
Bitmap afterBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
return afterBitmap;
}
/**
* 计算出压缩比
* @param options
* @param reqWith
* @param reqHeight
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight)
{
//通过参数options来获取真实图片的宽、高
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;//初始值是没有压缩的
if(width > reqWidth || height > reqHeight)
{
//计算出原始宽与现有宽,原始高与现有高的比率
int widthRatio = Math.round((float)width/(float)reqWidth);
int heightRatio = Math.round((float)height/(float)reqHeight);
//选出两个比率中的较小值,这样的话能够保证图片显示完全
inSampleSize = widthRatio < heightRatio ? widthRatio:heightRatio;
}
return inSampleSize;
}
/**
* 将InputStream转换为Byte数组
* @param in
* @return
*/
public static byte[] inputStreamToByteArray(InputStream in)
{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while((len = in.read(buffer)) != -1)
{
outputStream.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
in.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return outputStream.toByteArray();
}
static class ViewHolder
{
ImageView imageView;
TextView textView;
}
class ImageAsyncTask extends AsyncTask<String, Void, Bitmap>
{
public ListView listView;
public OnImageLoadListener listener;
public int position;
public ImageAsyncTask(ListView listView,int position)
{
this.listView = listView;
this.position = position;
}
public void setOnImageLoadListener(OnImageLoadListener listener)
{
this.listener = listener;
}
@Override
protected Bitmap doInBackground(String... params) {
String urlString = params[0];
URL url = null;
InputStream in = null;
HttpURLConnection connection = null;
Bitmap bitmap = null;
try {
url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
in = connection.getInputStream();
bitmap = decodeSampleBitmapFromStream(in, reqWidth, reqHeight);
//将当前Bitmap添加到缓存中
if(bitmap != null)
lruCache.put(urlString, bitmap);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
if(result != null)
{
listener.onSuccessLoad(result);
ImageView imageView = (ImageView) listView.findViewWithTag(position);
TextView textView = (TextView)listView.findViewWithTag(position+"#");
if(imageView != null)
imageView.setImageBitmap(result);
if(textView != null)
textView.setText("从网络中获取的");
}else
listener.onFailureLoad();
}
}
}
其中39行的getView方法代码就是我们平常使用ListView的常规写法,复用convertView以及对每个view设置viewHolder,为防止图片加载出现乱序,第55、56行我们分别对ImageView以及TextView设置了Tag标志;
第65行的addBitmapToMemoryCache用于向LruCache中添加当前Bitmap位图,因为LruCache本身实际上是由LinkedHashMap实现的,所有调用的是put方法;
第78行的getBitmapFromMemoryCache方法用于从LruCache缓存中读取指定key值的Bitmap位图;
第88行的loadImage就是我们的核心代码,首先第91行会通过调用getItem方法来获得当前item条目所要加载图片的url,随后93行调用getBitmapFromMemoryCache查看缓存中是否存在指定key值的Bitmap,第94行进行判断,如果存在Bitmap的话,则进入if语句块中,97、98行通过findViewWithTag来获得对应条目的ImageView以及TextView,并且将当前获取到的缓存图片显示到当前item;如果当前缓存中不存在的话,则进入103行的else语句块中,106行定义一个ImageAsyncTask图片加载线程,并在119行将当前线程加入到Set<ImageAsyncTask>类型的tasks集合中,便于我们随后对加载图片线程进行控制,第120行调用execute方法,将当前需要加载图片的url传入;
execute接下会调用ImageAsyncTask的doInBackground方法来加载图片,这里的图片加载方法和之前android-----带你一步一步优化ListView(二) 是一致的,不清楚的朋友可以看看上一篇博客,另外这里面也用到了图片压缩技术,不太懂的可以看看android-----解决Bitmap内存溢出的一种方法(图片压缩技术),在图片压缩完成之后会在第245行将当前Bitmap添加到LruCache中,随后在onPostExecute中进行更新UI的操作即可啦;
最后就是MainActivity方法了:
public class MainActivity extends Activity implements OnScrollListener {
public LruCache<String, Bitmap> memoryCache;
public int start_index;
public int end_index;
public ListViewAdapter adapter;
public boolean isInit = true;
public String[] images = { "https://img-my.csdn.net/uploads/201407/26/1406383299_1976.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383291_6518.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383291_8239.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383290_9329.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383290_1042.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383275_3977.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383265_8550.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383264_3954.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383264_4787.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383264_8243.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383248_3693.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383243_5120.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383242_3127.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383242_9576.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383242_1721.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383219_5806.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383214_7794.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383213_4418.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383213_3557.jpg",
"https://img-my.csdn.net/uploads/201407/26/1406383210_8779.jpg"};
public List<String> list = new ArrayList<String>();
public ListView listView;
public ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.listview);
memoryCache = getLruCache();
for(int i = 0;i < 50;i++)
{
int index = i %(images.length);
list.add(images[index]);
}
listView = (ListView) findViewById(R.id.listView);
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.item, null);
imageView = (ImageView) view.findViewById(R.id.imageView);
adapter = new ListViewAdapter(this, list, memoryCache, listView, imageView);
listView.setOnScrollListener(this);
listView.setAdapter(adapter);
}
/**
* 获得LruCache对象
* @return
*/
public LruCache<String, Bitmap> getLruCache()
{
LruCache<String, Bitmap> cache = null;
//获得可用的内存大小
int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024);
//将其1/8设置成我们的内存缓存大小
int cacheSize = maxMemory/8;
cache = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
//返回当前图片的大小(getByteCount用于返回当前位图所占用的内存字节数)
return value.getByteCount() / 1024;
}
};
return cache;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
start_index = firstVisibleItem;
end_index = start_index + visibleItemCount;
if(isInit == true && visibleItemCount > 0)
{
for(int i = start_index;i < end_index;i++)
{
adapter.loadImage(i);
}
isInit = false;
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE)
{
//表示停止滑动,这时候就可以加载图片
for(int i = start_index;i < end_index;i++)
{
adapter.loadImage(i);
}
}else
{
adapter.cancelTask();
}
}
}
这里比较关键的代码就是第55行的getLruCache方法了,首先会通过Runtime.getRuntime().maxMemory()获得当前可用的内存大小,之后的cacheSize就是我们自己设置的LruCache所用的内存大小,第66行的sizeof方法用于返回当前图片的大小;
同样,类似于上一篇中DiskLruCache的使用,这里我们也设置了ListView的滑动事件,保证其在滑动的过程中不会进行加载图片的请求操作,滑动停止再去加载图片;
另外提示一句,不要忘记添加访问网络的权限:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
点击下载源码!!!!!
我把上一篇博客和这篇的代码整合成了一个完整的带有DiskLruCache和LruCache缓存的ListView版本: