您的位置:首页 > 理论基础 > 计算机网络

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

2016-07-22 17:14 651 查看
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>>{

@Override
protected 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 implemen
10a2a
ts 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;
}
@Override
public int getCount() {
return mList.size();
}

@Override
public Object getItem(int position) {
return mList.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public 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;
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == SCROLL_STATE_IDLE){
//当前状态处于停止状态,加载可见项
imageLoader.loadImages(mStart, mEnd);
}else{
//停止任务
imageLoader.cancelAllTasks();
}
}
/**
* 由于我们使用的是滚动状态改变时才去下载图片,但是第一次进入的时候要加载第一屏的图片
* listview初始化后会调用onScroll方法,我们在这里去加载第一屏的图片并把第一次进入
* 状态位置为false
*/
@Override
public 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;
}
@Override
protected 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;
}
@Override
protected 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息