隔了很久没写博客,现在必须快速脉动回来。今天我还是接着上一个多线程中的异步加载系列中的最后一个使用异步加载实现ListView中的图片缓存及其优化。具体来说这次是一个综合Demo.但是个人觉得里面还算有点价值的就是里面的图片的缓存的实现。因为老实说它确实能在实际的项目中得到很好的应用。主要学习来源于慕课网中的异步加载学习,来自徐宜生大神的灵感。本次也就是对大神所讲知识的一个总结及一些个人的感受吧。
这次是一个综合的Demo,主要里面涉及到的知识主要有:网络编程、异步加载、JSON解析、图片缓存、通用ListAdapter的使用。最后实现一个加载网络数据的图文混排listView的效果。当然这里面涉及到的知识比较多,但是本次的重点就是图片缓存和异步加载,当然类似网络编程中的HttpURLConnection,JSON解析、打造通用适配器等知识将会在后续博客中给出,这里也就是使用我以前自己封装好的,因为为了简化开发。
这次的重点是异步加载和图片缓存,至于异步加载因为在前两个博客中已经写得很清楚了,这次主要是用一下异步加载,看看异步加载在实际项目是怎么使用的。主要是使用异步加载进行耗时网络请求,并且自定义一个监听器用于当获得数据后,立即将获得的数据回调出去。然后重点介绍的就是图片缓存。
说到图片缓存下面将通过以下几个方面认识一下图片缓存:
1、为什么要使用图片缓存?
很简单“消耗流量特别大” , 这个相信很多人都感同深受吧,因为我们可能都写过一个类似网络请求数据的ListView的图文混排的Demo,但是如果我们直接通过网络请求图片,然后拿到的图片显示在ListView上,当滑动ListView,下次将已经滑过Item,会发现图片重新请求一个网络数据,重新加载一次,也就是滑到哪就请求一次网络,不管是否重复。可想而知这流量消耗太大,估计这样滑一晚上,第二天早上醒来,发现自己的房子都成中国移动的了。还有一个弊端就是每请求一次网络都是一次异步和耗时过程,所以你会发现在滑动ListView会有卡顿情况出现。
2、图片缓存原理是什么?
图片缓存是基于LRU算法来实现的,LRU即Least Recently Used,中文意思是最近最少未使用算法,学过操作系统原理就知道这是操作系统中页面置换算法之一。
说到这,不妨来看看LruCache源码是怎么介绍的。
/** * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may * become eligible for garbage collection. * * <p>If your cached values hold resources that need to be explicitly released, * override {@link #entryRemoved}. * * <p>If a cache miss should be computed on demand for the corresponding keys, * override {@link #create}. This simplifies the calling code, allowing it to * assume a value will always be returned, even when there's a cache miss. * * <p>By default, the cache size is measured in the number of entries. Override * {@link #sizeOf} to size the cache in different units. For example, this cache * is limited to 4MiB of bitmaps: * <pre> {@code * int cacheSize = 4 * 1024 * 1024; // 4MiB * LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) { * protected int sizeOf(String key, Bitmap value) { * return value.getByteCount(); * } * }}</pre> * * <p>This class is thread-safe. Perform multiple cache operations atomically by * synchronizing on the cache: <pre> {@code * synchronized (cache) { * if (cache.get(key) == null) { * cache.put(key, value); * } * }}</pre> * * <p>This class does not allow null to be used as a key or value. A return * value of null from {@link #get}, {@link #put} or {@link #remove} is * unambiguous: the key was not in the cache. * * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's * Support Package</a> for earlier releases. */
LruCache主要原理:缓存是限制了缓存的数目的,也就是说缓存的容量是有限的,可以把缓存的逻辑内存结构想象一个队列,当缓存中一个缓存值被访问后,它将会被置换到队列的队头,当一个缓存值需要加到队尾时,但是此时队列已满了,也即此时缓存空间已满,那么就需要将处于队列队尾一个缓存值出队列,也即是释放队列队尾一部分缓存空间,因为基于LRU算法处于队尾的,肯定最近最少未使用。也就是因为缓存空间是有限的,才会基于这样算法,及时并合适地将一些数据空间释放。
LruCache类是线程安全的,它支持多个缓存操作自动通过异步来实现,并且这个类不允许用空值去作为key或者value,并且注意LruCache的key不是保存在缓存中的。
LurCache类出现在Android3.1版本。
3、LruCache如何创建:
LruCache实际在操作上很类似于Map的操作,初学者实际上就可以把它当做一个Map,因为它是key-value成对的,并且有put(),get()方法非常类似Map
个人觉得使用图片缓存使用率很高,为了下次方便使用,索性直接将它封装成一个工具类。
package com.mikyou.utils; import android.graphics.Bitmap; import android.util.LruCache; public class LruCacheUtils { //创建Cache缓存,第一个泛型表示缓存的标识key,第二个泛型表示需要缓存的对象 private LruCache<String, Bitmap> mCaches; public LruCacheUtils() { int maxMemory=(int) Runtime.getRuntime().maxMemory();//获取最大的应用运行时的最大内存 //通过获得最大的运行时候的内存,合理分配缓存的内存空间大小 int cacheSize=maxMemory/4;//取最大运行内存的1/4; mCaches=new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap value) {//加载正确的内存大小 return value.getByteCount();//在每次存入缓存的时候调用 } }; } //将图片保存在LruCache中 public void addBitmapToCache(String url,Bitmap bitmap){ if (getBitmapFromCache(url)==null) {//判断当前的Url对应的Bitmap是否在Lru缓存中,如果不在缓存中,就把当前url对应的Bitmap对象加入Lru缓存 mCaches.put(url, bitmap); } } //将图片从LruCache中读取出来 public Bitmap getBitmapFromCache(String url){ Bitmap bitmap=mCaches.get(url);//实际上LruCache就是一个Map,底层是通过HashMap来实现的 return bitmap; } }
通过以上知识的讲解,相信已经对LruCache有了一定的了解了,那么接下来我们就开始我们的Demo吧。
1、首先、我们既然是加载网络数据,所以得解决网络数据来源问题,主要来自于慕课网的一个课程列表的API的地址,返回的数据是JSON格式的数据。
地址是:http://www.imooc.com/api/teacher?type=4&num=60。可以先用浏览器来测试一下数据,测试结果如下:
注意:大家可能看到这里面中文全部都乱码了,这是因为Unicode编码,我会在代码中使用一个工具类将这些转化成中文。
2、数据解决后,那么接着就是布局,布局很简单,主布局就是一个ListView,listItem布局也很简单。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="wrap_content" android:divider="#22000000" android:dividerHeight="0.2dp" > </ListView> </RelativeLayout>
<?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="wrap_content" android:padding="5dp" > <ImageView android:id="@+id/c_img" android:layout_width="140dp" android:layout_height="90dp" android:src="@drawable/left_img" /> <TextView android:id="@+id/c_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Android百度地图之导航" android:textSize="16sp" android:layout_marginLeft="5dp" android:layout_toRightOf="@id/c_img" android:layout_marginTop="10dp" /> <TextView android:id="@+id/c_learner" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableLeft="@drawable/learner" android:text="7897" android:textColor="#a9b7b7" android:layout_alignBottom="@id/c_img" android:layout_alignLeft="@id/c_name" android:drawablePadding="5dp" android:layout_marginBottom="5dp" /> </RelativeLayout>
3、自己封装的HttpURLConnection网络请求框架,返回的是整个JSON数据
package com.mikyou.utils; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import org.json.JSONObject;import android.R.interpolator; public class MikyouHttpUrlConnectionUtils { private static StringBuffer buffer; public static String getData(String urlString,String apiKeyValue,List<String> stringList){ buffer=new StringBuffer(); String jsonOrXmlString=null; if (stringList!=null) { for (int i = 0; i <stringList.size(); i++) { urlString+=stringList.get(i); } } try { System.out.println("URL---->"+urlString); URL url=new URL(urlString); HttpURLConnection conn=(HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); if (apiKeyValue!=null) { conn.setRequestProperty("apikey", apiKeyValue); } conn.setConnectTimeout(8000); conn.setReadTimeout(8000); conn.connect(); if (conn.getResponseCode()==200) { InputStream is=conn.getInputStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(is, "UTF-8")); while ((jsonOrXmlString=reader.readLine())!=null) { buffer.append(jsonOrXmlString+"\n"); } reader.close(); is.close(); } } catch (Exception e) { e.printStackTrace(); } String string=UnicodeUtils.decodeUnicode(buffer.toString());//使用UnicodeUtils工具类将Unicode编码转换成UTF-8显示的中文 return string; } }
4、封装课程对象的javaBean类对象即每个Item为一个对象
package com.mikyou.bean; import java.io.Serializable; import android.R.id; public class Course implements Serializable{ private String cName; private String cImgURl; private String cDescriptor; private String cLearner; public String getcName() { return cName; } public void setcName(String cName) { this.cName = cName; } public String getcImgURl() { return cImgURl; } public void setcImgURl(String cImgURl) { this.cImgURl = cImgURl; } public String getcDescriptor() { return cDescriptor; } public void setcDescriptor(String cDescriptor) { this.cDescriptor = cDescriptor; } public String getcLearner() { return cLearner; } public void setcLearner(String cLearner) { this.cLearner = cLearner; } }
5、通用适配器实现的子类
package com.mikyou.adapter; import java.util.List; import com.lidroid.xutils.BitmapUtils; import com.mikyou.async.ImageLoader; import com.mikyou.bean.Course; import com.mikyou.cache.R; import com.mikyou.tools.ViewHolder; import android.content.Context; import android.widget.ImageView; public class MyListAdapter extends CommonAdapter<Course>{ private ImageLoader loader; public MyListAdapter(Context context, List<Course> listBeans, int layoutId) { super(context, listBeans, layoutId); loader=new ImageLoader(); } @Override public void convert(ViewHolder holder, Course course) { holder.setText(R.id.c_name, course.getcName()).setText(R.id.c_learner, course.getcLearner()); ImageView iv= holder.getView(R.id.c_img); iv.setTag(course.getcImgURl());//首先、需要将相应的url和相应的iv绑定在一起,为了防止图片和请求URL不对应 loader.showImageByAsyncTask(iv, course.getcImgURl()); } }
7、核心实现代码:
package com.mikyou.async; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import com.mikyou.bean.Course; import com.mikyou.utils.LruCacheUtils; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Handler; import android.os.Message; import android.util.Log; import android.util.LruCache; import android.widget.ImageView; public class ImageLoader { private ImageView iv; private String url; private LruCacheUtils mCacheUtils; public ImageLoader() { mCacheUtils=new LruCacheUtils(); } /** * @author mikyou * 实现的主要思路: * 首先、加载图片的时候,先去LruCache缓存中根据传入的url作为key去取相应的Bitmap对象 * ,如果缓存中存在相应的key对应的value,那么就直接取出key对应缓存中的Bitmap对象 * 并设置给ImageView,如果缓存中没有,那么就需要通过异步加载请求网络中的数据和图片信息, * 然后通过监听器中的asyncImgListener回调方法将网络请求得到的Bitmap对象,首先得通过iv.getTag() * 比较url如果对应就将该Bitmap对象设置给iv,并且还需要将这个Bitmap对象和相应的url以key-value形式 * 通过put方法,加入LruCache缓存中。 * */ public void showImageByAsyncTask(final ImageView iv,final String url){ //首先,从缓存中读取图片,如果有就直接使用缓存,如果没有就直接加载网络图片 Bitmap bitmap=mCacheUtils.getBitmapFromCache(url); Log.d("url", url); if (bitmap==null) {//表示缓存中没有,就去访问网络下载图片,并记住将下载到的图片放入缓存中 ImageAsyncTask imageAsyncTask=new ImageAsyncTask(); imageAsyncTask.execute(url); imageAsyncTask.setOnImgAsyncTaskListener(new OnAsyncListener() { @Override public void asyncListener(List<Course> mCourseList) { } @Override public void asyncImgListener(Bitmap bitmap) {//图片请求网络数据的回调方法 if (iv.getTag().equals(url)) {//判断url和iv是否对应 iv.setImageBitmap(bitmap); Log.d("addLru", "网络加载并加入缓存--->"+url); mCacheUtils.addBitmapToCache(url, bitmap);//由于是网络请求得到的数据,所以缓存中肯定没有,所以还需要将该Bitmap对象加入到缓存中 } } }); }else{//否则就直接从缓存中获取 iv.setImageBitmap(mCacheUtils.getBitmapFromCache(url));//直接读取缓存中的Bitmap对象 Log.d("getLru", "url读出缓存--->"+url); } } //HttpURLConnection网络请求方式来得到网络图片输入流,并且将输入流转换成一个Bitmap对象 public Bitmap getBitmapFromURL(String url){ Bitmap bitmap = null; try { URL mURL=new URL(url); HttpURLConnection conn=(HttpURLConnection) mURL.openConnection(); bitmap = BitmapFactory.decodeStream(conn.getInputStream()); conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } return bitmap; } }
8、异步加载类实现,这里主要有两个:一个是请求整个网络的JSON数据,另一个就是请求加载网络图片,并且自定义一个监听器接口。
package com.mikyou.async; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.mikyou.bean.Course; import com.mikyou.utils.MikyouHttpUrlConnectionUtils; import android.os.AsyncTask; import android.util.Log; public class MikyouAsyncTask extends AsyncTask<String, Void, String>{ private List<Course> mCourseList; private OnAsyncListener listener;//自定义监听器接口对象引用 @Override protected void onPreExecute() { mCourseList=new ArrayList<Course>(); super.onPreExecute(); } @Override protected String doInBackground(String... params) { String data=MikyouHttpUrlConnectionUtils.getData(params[0], null, null);//网络请求JSON数据 return data; } @Override protected void onPostExecute(String result) {//解析JSON数据 Log.d("info", result); try { JSONObject object=new JSONObject(result); JSONArray array=object.getJSONArray("data"); for (int i = 0; i < array.length(); i++) { Course mCourse=new Course(); JSONObject object2=array.getJSONObject(i); mCourse.setcName(object2.getString("name")); mCourse.setcImgURl(object2.getString("picSmall")); mCourse.setcLearner(object2.getInt("learner")+""); mCourse.setcDescriptor(object2.getString("description")); mCourseList.add(mCourse); } if (listener!=null) {//判断是否注册了监听器 listener.asyncListener(mCourseList);//通过监听器中的回调方法将异步加载得到的数据后经过解析、封装的对象集合回调出去 } } catch (JSONException e) { e.printStackTrace(); } super.onPostExecute(result); } public void setOnAsyncTaskListener(OnAsyncListener listener){//公布一个注册监听器的方法 this.listener=listener; } }
ImgAsyncTask异步加载类:
package com.mikyou.async; import java.net.HttpURLConnection; import java.net.URL; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.Image; import android.os.AsyncTask; import android.text.GetChars; import android.util.LruCache; import android.widget.ImageView; public class ImageAsyncTask extends AsyncTask<String, Void, Bitmap>{ private OnAsyncListener listener; @Override protected Bitmap doInBackground(String... params) { return getBitmapFromURL(params[0]); } @Override protected void onPostExecute(Bitmap result) { if (listener!=null) { listener.asyncImgListener(result); } super.onPostExecute(result); } public Bitmap getBitmapFromURL(String url){ Bitmap bitmap = null; try { URL mURL=new URL(url); HttpURLConnection conn=(HttpURLConnection) mURL.openConnection(); bitmap = BitmapFactory.decodeStream(conn.getInputStream()); conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } return bitmap; } public void setOnImgAsyncTaskListener(OnAsyncListener listener){ this.listener=listener; } }
自定义监听器:
监听器接口:
package com.mikyou.async; import java.util.List; import com.mikyou.bean.Course; import android.graphics.Bitmap; public interface OnAsyncListener { public void asyncListener(List<Course> mCourseList); public void asyncImgListener(Bitmap bitmap); }
运行结果:
没有加入图片缓存的运行结果会发现无论什么时候滑动都会请求网络,会发现图片加载有个延迟时间:
加入图片缓存后的运行结果会发现,非常流畅,并且直接读缓存的图片时没有图片加载的延迟
最后,图片缓存LruCache实际上运用很流行,并且运用在很多流行网络框架中,我们都知道很流行的Xutils框架,其中就有一个BitmapUtils,它里面实现缓存原理也就是基于LruCache来实现的。