【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,该ListView的Item布局为
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.javapublic 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
- 【Android进阶】ListView使用“内存双缓存+硬盘缓存”加载网络图片
- Android之ListView异步加载网络图片(优化缓存机制)和对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存
- Android ListView 图片异步加载和图片内存缓存
- Android ListView 图片异步加载和图片内存缓存机制
- Android ListView 图片异步加载和图片内存缓存
- Android ListView 图片异步加载和图片内存缓存
- Android ListView 图片异步加载和图片内存缓存
- Android ListView 图片异步加载和图片内存缓存
- android 网络加载图片,对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存
- android 网络加载图片,对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存
- android 网络加载图片,对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存
- android 网络加载图片,对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存
- android 网络加载图片,对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存
- android 网络加载图片,对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存
- ListView 图片加载+内存缓存+图片压缩
- Android之ListView异步加载网络图片(优化缓存机制)
- Android之ListView异步加载网络图片(优化缓存机制) .
- Android之ListView异步加载网络图片(优化缓存机制)
- handleBars
- Oracle数据库性能调优(上)
- [算法学习笔记]分治法——最大子序列和问题
- CodeForces 349B
- RMQ算法
- 【Android进阶】ListView使用“内存双缓存+硬盘缓存”加载网络图片
- MFC学习笔记——对话框:路径选择对话框(BROWSEINFO)
- Ubuntu快速上手指南(新手教程)
- 基于Linux上的wifi密码爆破
- 装了多个版本的sqlserver,无法共享1433端口,可以自己指定端口
- Java中String与byte[]的转换
- Unity5学习心得-旋转
- java UnicodeHtml
- mysql when then 用法