【Android进阶】ListView使用“内存双缓存+硬盘缓存”加载网络图片

来源:互联网 发布:淘宝客服工作好不好干 编辑:程序博客网 时间:2024/06/14 00:17

ListView 加载网络图片是我们经常用到的方式,如果每次滚动ListView就去网络下载图片会非常影响性能(因为网络下载是比较慢的)而且非常耗费流量,所以这里介绍一种使用“内存双缓存+硬盘缓存”的方式来加载图片。

实现的效果如下:

这里使用了滚动时不去网络下载图片,停止时才加载,所以滚动时显示默认的,注意观察


设计思想

内存读取速度 > 文件读取速度> 从网络获取的速度

基本代码逻辑如下

// 从内存缓存中获取图片          Bitmap result = memoryCache.getBitmapFromCache(url);          if (result == null) {              // 从文件缓存中获取              result = fileCache.getImage(url);              if (result == null) {                  // 从网络获取                  result = ImageGetFromHttp.downloadBitmap(url);                  if (result != null) {                      fileCache.saveBitmap(result, url);                      memoryCache.addBitmapToCache(url, result);                  }              } else {                  // 添加到内存缓存                  memoryCache.addBitmapToCache(url, result);              }        }

内存缓存中使用了LruCache,LRU算法请参考:http://blog.csdn.net/luoweifu/article/details/8297084/

我们在内存缓存中再将内存分为两层,强引用缓存和软引用缓存。

对强引用和软引用做简单的介绍(具体内容请看:http://blog.csdn.net/u010583599/article/details/51970515

① 强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

② 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

程序介绍

主界面是一个ListView,该ListViewItem布局为

Item_layout.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="wrap_content"    android:orientation="horizontal" >    <ImageView        android:id="@+id/iv_icon"         android:layout_width="64dp"        android:layout_height="64dp"        android:src="@drawable/ic_launcher"/>    <LinearLayout         android:layout_width="match_parent"        android:layout_height="match_parent"        android:gravity="center"        android:paddingLeft="5dp"        android:orientation="vertical">        <TextView             android:id="@+id/tv_title"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:textSize="15sp"            android:text="新闻的标题"/>         <TextView             android:id="@+id/tv_content"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:maxLines="3"            android:text="新闻的内容"/>            </LinearLayout></LinearLayout>

Activity完成的功能是,请求并解析慕课网的接口的JSon字符串,创建ListView的适配器。

MainActivity.java

/** * 主类,访问网络接口获取JSon字符串,并解析字符串,生成对象集合,创建listView适配器 * */public class MainActivity extends Activity {private ListView listView;//网络接口来自慕课网,获取一个Json字符串并解析private static final String URL = "http://www.imooc.com/api/teacher?type=4&num=30";    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        listView = (ListView)findViewById(R.id.listview);        new NewsAsyncTask().execute(URL);    }    private List<NewsBean> getJsonData(String url){    List<NewsBean> newsBeansList = new ArrayList<NewsBean>();    try {    //获得json数据并解析Json字符串String jsonString = readStream(new URL(url).openStream());JSONObject jsonObject;NewsBean newsBean;jsonObject = new JSONObject(jsonString);JSONArray jsonArray = jsonObject.getJSONArray("data");for(int i = 0;i< jsonArray.length();i++){jsonObject = jsonArray.getJSONObject(i);newsBean = new NewsBean();newsBean.newsIconUrl = jsonObject.getString("picSmall");newsBean.newsTitle = jsonObject.getString("name");newsBean.newsContent = jsonObject.getString("description");newsBeansList.add(newsBean);}} catch (MalformedURLException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (JSONException e) {e.printStackTrace();}    return newsBeansList;    }    //获取网络输入流数据,返回一个Json字符串    private String readStream(InputStream is){    InputStreamReader isr;    String result = "";    try{    String line = "";    isr = new InputStreamReader(is,"utf-8");    BufferedReader br = new BufferedReader(isr);    while((line = br.readLine()) != null){    result += line;    }    }catch(IOException e){    e.printStackTrace();    }    return result;    }    /**     * 使用AsyncTask来访问网络获取JSon数据     *     */    class NewsAsyncTask extends AsyncTask<String , Void, List<NewsBean>>{@Overrideprotected List<NewsBean> doInBackground(String... params) {return getJsonData(params[0]);}    @Override    protected void onPostExecute(List<NewsBean> result) {    super.onPostExecute(result);    //创建并给listView设置适配器    NewsAdapter adapter = new NewsAdapter(getApplicationContext(), result,listView);    listView.setAdapter(adapter);    }    }}

为了解析Json字符串,我们需要创建一个实体类

NewsBean.java

public class NewsBean {public String newsIconUrl;public String newsTitle;public String newsContent;public NewsBean(String newsIconUrl,String newsTitle,String newsContent){this.newsIconUrl = newsIconUrl;this.newsTitle = newsTitle;this.newsContent = newsContent;}public NewsBean(){}}

ListView 适配器类中我们完成了图片的下载,并且进行了控制,ListView滑动时不进行任何的下载,停止状态才进行网络下载,并且进行了优化,防止图片加载时错位、重复、闪烁

NewsAdapter.java
public class NewsAdapter extends BaseAdapter implements OnScrollListener{private List<NewsBean> mList;private LayoutInflater mInflater;//图片加载类private ImageLoader imageLoader;//listView开始下载和结束下载的位置private int mStart,mEnd;//所有的URL的数组public static String[] URLS;private boolean mFirstIn;//第一次启动public NewsAdapter(Context context,List<NewsBean> mList,ListView listView){mInflater = LayoutInflater.from(context);this.mList = mList;imageLoader = new ImageLoader(listView,context);//获取所有的URL并初始化数组URLS = new String[mList.size()];for(int i = 0;i<mList.size();i++){URLS[i] = mList.get(i).newsIconUrl;}listView.setOnScrollListener(this);mFirstIn = true;}@Overridepublic int getCount() {return mList.size();}@Overridepublic Object getItem(int position) {return mList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder viewHolder = null;if(convertView == null){viewHolder = new ViewHolder();convertView = mInflater.inflate(R.layout.item_layout, null);viewHolder.ivIcon = (ImageView)convertView.findViewById(R.id.iv_icon);viewHolder.tvTitle = (TextView)convertView.findViewById(R.id.tv_title);viewHolder.tvContent = (TextView)convertView.findViewById(R.id.tv_content);convertView.setTag(viewHolder);}else{viewHolder = (ViewHolder) convertView.getTag();}//设置默认的图片//viewHolder.ivIcon.setImageResource(R.drawable.ic_launcher);String url = mList.get(position).newsIconUrl;/*因为Itme是重复利用的,ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行, * 且滑动过程中该图片加载结束,第2行已不在屏幕内,根据缓存原理,第2行的view可能被第14行复用,这样我 * 们看到的就是第14行显示了本该属于第2行的图片,造成显示重复。如果14行的图片也加载结束则会造成闪烁,先显示前一张,再显示后一张 * 为了防止图片加载时错位,这里加上tag,把imageView和url标识绑定,在异步显示的位置,判断当前任务的url和item设置的url是否 * 相同,只有相同才去加载图片 */viewHolder.ivIcon.setTag(url);//在滚动的时候加载图片,如果缓存中都没有,则使用默认的图片imageLoader.showImagesFromCache(viewHolder.ivIcon, url);viewHolder.tvTitle.setText(mList.get(position).newsTitle);viewHolder.tvContent.setText(mList.get(position).newsContent);return convertView;}class ViewHolder{public TextView tvTitle,tvContent;public ImageView ivIcon;}@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState) {if(scrollState == SCROLL_STATE_IDLE){//当前状态处于停止状态,加载可见项imageLoader.loadImages(mStart, mEnd);}else{//停止任务imageLoader.cancelAllTasks();}}/** * 由于我们使用的是滚动状态改变时才去下载图片,但是第一次进入的时候要加载第一屏的图片 * listview初始化后会调用onScroll方法,我们在这里去加载第一屏的图片并把第一次进入 * 状态位置为false */@Overridepublic void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {//start 为第一个可见的item的位置mStart = firstVisibleItem;//end 为第一个可见的位置加上可见的item的数量mEnd = firstVisibleItem + visibleItemCount;if(mFirstIn && visibleItemCount > 0){//第一次显示的时候调用,加载图片imageLoader.loadImages(mStart, mEnd);mFirstIn = false;}}}
内存缓存类 ImageMemoryCache.java

/** * @author meng.li * 内存缓存图片类,这里使用了两层内存缓存 */public class ImageMemoryCache {/**      * 从内存读取数据速度是最快的,为了更大限度使用内存,这里使用了两层缓存。      * 强引用缓存不会轻易被回收,用来保存常用数据     * 不常用的数据转入软引用缓存,不会影响GC的回收。      */      private static final int SOFT_CACHE_SIZE = 15;  //软引用缓存容量      private static LruCache<String, Bitmap> mLruCache;  //硬引用缓存      private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache;  //软引用缓存                                                                                                  public ImageMemoryCache(Context context) {      //获取最大可用内存    int maxMemory = (int) Runtime.getRuntime().maxMemory();    Log.i("mengli","maxMemory = "+ maxMemory);    //强引用缓存容量,为系统可用内存的1/4     int cacheSize = maxMemory/4;        mLruCache = new LruCache<String, Bitmap>(cacheSize) {              @Override              protected int sizeOf(String key, Bitmap value) {                  if (value != null)                  //每次加入缓存时会调用    return value.getByteCount();                else                      return 0;              }            @Override              protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {                  if (oldValue != null)                //LRU算法会把最近使用的元素压入栈顶,所以栈底就是被移除的元素                    // 强引用缓存容量满的时候,会根据LRU算法把最近最久没有被使用的图片转入此软引用缓存                      mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));              }          };          mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(SOFT_CACHE_SIZE, 0.75f, true) {  //            private static final long serialVersionUID = 6040103833179403725L;              @Override              protected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>> eldest) {                  if (size() > SOFT_CACHE_SIZE){                          return true;                    }                    return false;               }          };      }                                                                                          /**      * 从缓存中获取图片      */      public Bitmap getBitmapFromCache(String url) {          Bitmap bitmap;          //先从强引用缓存中获取          synchronized (mLruCache) {              bitmap = mLruCache.get(url);              if (bitmap != null) {                  //如果找到的话,把元素移到LinkedHashMap的最前面,从而保证在LRU算法中最后被删除                  mLruCache.remove(url);                  mLruCache.put(url, bitmap);                  return bitmap;              }          }          //如果强引用缓存中找不到,到软引用缓存中找         synchronized (mSoftCache) {               SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);              if (bitmapReference != null) {                  bitmap = bitmapReference.get();                  if (bitmap != null) {                      //将图片移回硬缓存                      mLruCache.put(url, bitmap);                      mSoftCache.remove(url);                      return bitmap;                  } else {                //没找到,可能改bigmap已经被回收了,删除url                    mSoftCache.remove(url);                  }              }          }          return null;      }                                                                                           /**      * 添加图片到缓存      */      public void addBitmapToCache(String url, Bitmap bitmap) {          if (bitmap != null) {              synchronized (mLruCache) {                  mLruCache.put(url, bitmap);              }          }      }                                                                                          public void clearCache() {          mSoftCache.clear();      }  }
文件缓存类 ImageFileCache.java

/** * @author meng.li *  文件缓存类 */public class ImageFileCache {//缓存目录名    private static final String CACHDIR = "ImgeCache";      private static final String WHOLESALE_CONV = ".cach";                                                                    private static final int MB = 1024*1024;    //缓存大小    private static final int CACHE_SIZE = 10;     //剩余最小空间大小    private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 10;                                                                        public ImageFileCache() {          //清理文件缓存          removeCache(getDirectory());      }                                                                        /** 从缓存中获取图片 **/      public Bitmap getImage(final String url) {        final String path = getDirectory() + "/" + getFileNameFromUrl(url);          File file = new File(path);          if (file.exists()) {              Bitmap bmp = BitmapFactory.decodeFile(path);              if (bmp == null) {                  file.delete();              } else {            //获取时图片时需要更新文件的最后修改时间                updateFileTime(path);                  return bmp;              }          }          return null;      }                                                                      /** 将图片存入文件缓存 **/      public void saveBitmap(Bitmap bm, String url) {          if (bm == null) {              return;          }          //判断sdcard上的空间 ,如果不足10M返回        if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {              //SD空间不足              return;          }          String filename = getFileNameFromUrl(url);          String dir = getDirectory();          File dirFile = new File(dir);          if (!dirFile.exists())              dirFile.mkdirs();        //创建文件        File file = new File(dir +"/" + filename);          try {              file.createNewFile();              OutputStream outStream = new FileOutputStream(file);            //将图片进行压缩并写入文件,100表示不压缩            bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream);              outStream.flush();              outStream.close();          } catch (FileNotFoundException e) {              Log.w("ImageFileCache", "FileNotFoundException");          } catch (IOException e) {              Log.w("ImageFileCache", "IOException");          }      }                                                                         /**      * 计算存储目录下的文件大小,      * 当文件总大小大于规定的CACHE_SIZE或者sdcard剩余空间小于FREE_SD_SPACE_NEEDED_TO_CACHE的规定      * 那么删除40%最近没有被使用的文件      */      private boolean removeCache(String dirPath) {          File dir = new File(dirPath);          File[] files = dir.listFiles();          if (files == null) {              return true;          }        //没有挂载外部存储设备        if (!android.os.Environment.getExternalStorageState().equals(                  android.os.Environment.MEDIA_MOUNTED)) {              return false;          }                                                                      int dirSize = 0;          for (int i = 0; i < files.length; i++) {        //遍历目录下的所有文件,如果包含.cach 则累加size            if (files[i].getName().contains(WHOLESALE_CONV)) {                  dirSize += files[i].length();              }          }        //如果缓存目录的文件大小 大于规定的缓存大小或者剩余内存不足10M,则删除40%最久未使用的文件        if (dirSize > CACHE_SIZE * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {              int removeFactor = (int) ((0.4 * files.length) + 1);            //对文件按时间排序            Arrays.sort(files, new FileLastModifSort());            for (int i = 0; i < removeFactor; i++) {                  if (files[i].getName().contains(WHOLESALE_CONV)) {                      files[i].delete();                  }              }          }                                                                        if (freeSpaceOnSd() <= CACHE_SIZE) {              return false;          }                                                                                return true;      }                                                                        /** 修改文件的最后修改时间 **/      public void updateFileTime(String path) {          File file = new File(path);          long newModifiedTime = System.currentTimeMillis();          file.setLastModified(newModifiedTime);      }                                                                        /** 计算sdcard上的剩余空间 **/      private int freeSpaceOnSd() {          StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());          double sdFreeMB = ((double)stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;          return (int) sdFreeMB;      }                                                                         /**从url中获取文件名 **/      private String getFileNameFromUrl(String url) {          return url.substring(url.lastIndexOf("/")+1)+WHOLESALE_CONV;    }                                                                        /** 获得缓存目录 **/      private String getDirectory() {          String dir = getSDPath() + "/" + CACHDIR;          return dir;      }                                                                        /** 取SD卡路径 **/      private String getSDPath() {          File sdDir = null;          boolean sdCardExist = Environment.getExternalStorageState().equals(                  android.os.Environment.MEDIA_MOUNTED);  //判断sd卡是否存在          if (sdCardExist) {              sdDir = Environment.getExternalStorageDirectory();  //获取根目录          }          if (sdDir != null) {              return sdDir.toString();          } else {              return "";          }      }       /**      * 根据文件的最后修改时间进行排序,Java中对对象进行排序要实现Comparator 接口,自己实现比较规则     * 1 表示大于,0表示相等,-1表示小于      */      private class FileLastModifSort implements Comparator<File> {          public int compare(File arg0, File arg1) {              if (arg0.lastModified() > arg1.lastModified()) {                  return 1;              } else if (arg0.lastModified() == arg1.lastModified()) {                  return 0;              } else {                  return -1;              }          }      }  }  

通过图片我们可以看到,图片被写入了文件


图片下载类,使用多线程下载图片用了AsyncTask封装类

public class ImageLoader {/** * 使用多线程的方式去加载图片 */private ImageView imageView;private String mUrl;//内存缓存private ImageMemoryCache memoryCache;//文件缓存private ImageFileCache fileCache;private ListView mListView;//任务集合,用来处理多个下载线程private Set<NewsAsyncTask> mTasks;public ImageLoader(ListView listView,Context context){mListView = listView;mTasks = new HashSet<ImageLoader.NewsAsyncTask>();memoryCache = new ImageMemoryCache(context);fileCache = new ImageFileCache();}/** * 用于从一个url获取bitmap */public Bitmap getBitmapFromURL(String urlString){Bitmap bitmap;InputStream is = null;try {URL url = new URL(urlString);HttpURLConnection connection = (HttpURLConnection)url.openConnection();is = new BufferedInputStream(connection.getInputStream());bitmap = BitmapFactory.decodeStream(is);connection.disconnect();return bitmap;} catch (Exception e) {}finally{try {is.close();} catch (IOException e) {e.printStackTrace();}}return null;}//在滚动的时候显示缓存的图片,如果没有缓存图片则显示默认的图片public void showImagesFromCache(ImageView imageView,String url){//从缓存取出图片Bitmap result = memoryCache.getBitmapFromCache(url);          if (result == null) {            // 文件缓存中获取              result = fileCache.getImage(url);        }if(result == null){imageView.setImageResource(R.drawable.ic_launcher);}else{imageView.setImageBitmap(result);}}//取消加载图片public void cancelAllTasks(){if(mTasks != null){for(NewsAsyncTask task: mTasks){task.cancel(false);}}}public void loadImages(int start,int end){//加载从start到end的图片for(int i = start;i<end;i++){String url = NewsAdapter.URLS[i];//从内存缓存取出图片Bitmap bitmap = memoryCache.getBitmapFromCache(url);//如果缓存没有,则从文件中读取if(bitmap == null){//从文件获取图片bitmap = fileCache.getImage(url);//文件中也为空,则必须从网络下载图片if(bitmap == null){//使用AsyncTask下载图片,这里会耗费流量NewsAsyncTask task = new NewsAsyncTask(url);task.execute(url);//添加一个任务mTasks.add(task);}else{//文件中获取到了图片,则把图片加入到内存中memoryCache.addBitmapToCache(url, bitmap);}}if(bitmap != null){//根据url去获取 对应的imageView对象,防止显示混乱ImageView imageView = (ImageView) mListView.findViewWithTag(url);imageView.setImageBitmap(bitmap);}}}private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap>{private String mUrl;public NewsAsyncTask(String url){mUrl = url;}@Overrideprotected Bitmap doInBackground(String... params) {//从网络获取图片Bitmap bitmap = getBitmapFromURL(params[0]);if(bitmap != null){//把bitmap加入到缓存memoryCache.addBitmapToCache(params[0], bitmap);//把bitmap 加入到文件fileCache.saveBitmap(bitmap,params[0]);}return bitmap;}@Overrideprotected void onPostExecute(Bitmap result) {super.onPostExecute(result);ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);if(imageView != null && result != null){imageView.setImageBitmap(result);}//下载任务完成则移除这个任务mTasks.remove(this);}}}

AndroidManifest.xml  注意加上权限

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.newsdemo"    android:versionCode="1"    android:versionName="1.0" >    <uses-sdk        android:minSdkVersion="22"        android:targetSdkVersion="22" /><uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <application        android:allowBackup="true"        android:icon="@drawable/ic_launcher"        android:label="@string/app_name"        android:theme="@android:style/Theme.Black.NoTitleBar" >        <activity            android:name=".MainActivity"            android:label="@string/app_name" >            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application></manifest>

菜鸟一只,刚踏上追求技术的不归路,记录所学,希望大家指点!

代码下载 :http://download.csdn.net/detail/u010583599/9583583

本文参考:

http://blog.csdn.net/a79412906/article/details/10180583

http://www.imooc.com/learn/406









0 0
原创粉丝点击