Listview的使用与优化(下)

接上一篇文章,首先我们再来复习一个listview的缓存优化方法

1,对Imageview使用setTag()方法来解决图片错位问题,这个Tag中设置的是图片的url,然后在加载的时候取得这个url和要加载那position中的url对比,如果不相同就加载,相同就是复用以前的就不加载了

2,对于要加载的图片资源,先在内存缓存中找(原始的方法是使用SoftRefrence,最新的方法是使用android提供的Lrucache),如果找不到,则在本地缓存(可以使用DiskLrucache类)中找(也就是读取原先下载过的本地图片),还找不到,就开启异步线程去下载图片,下载以后,保存在本地,内存缓存也保留一份引用

3,在为imagview装载图片时,先测量需要的图片大小,按比例缩放

4,使用一个Map保存异步线程的引用,key->value为url->AsyncTask,这样可以避免已经开启了线程去加载图片,但是还没有加载完时,又重复开启线程去加载图片的情况

5,在快速滑动的时候不加载图片,取消所有图片加载线程,一旦停下来,继续可见图片的加载线程

下面来看第三个例子

FileUtils 文件操作的工具类,提供保存图片,获取图片,判断图片是否存在,删除图片的一些方法,这个类比较简单

package com.example.asyncimageloader;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;

public class FileUtils {
	/**
	 * sd卡的根目录
	 */
	private static String mSdRootPath = Environment.getExternalStorageDirectory().getPath();
	/**
	 * 手机的缓存根目录
	 */
	private static String mDataRootPath = null;
	/**
	 * 保存Image的目录名
	 */
	private final static String FOLDER_NAME = "/AndroidImage";

	public FileUtils(Context context){
		mDataRootPath = context.getCacheDir().getPath();
	}

	/**
	 * 获取储存Image的目录
	 * @return
	 */
	private String getStorageDirectory(){
		return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ?
				mSdRootPath + FOLDER_NAME : mDataRootPath + FOLDER_NAME;
	}

	/**
	 * 保存Image的方法,有sd卡存储到sd卡,没有就存储到手机目录
	 * @param fileName
	 * @param bitmap
	 * @throws IOException
	 */
	public void savaBitmap(String fileName, Bitmap bitmap) throws IOException{
		if(bitmap == null){
			return;
		}
		String path = getStorageDirectory();
		File folderFile = new File(path);
		if(!folderFile.exists()){
			folderFile.mkdir();
		}
		File file = new File(path + File.separator + fileName);
		file.createNewFile();
		FileOutputStream fos = new FileOutputStream(file);
		bitmap.compress(CompressFormat.JPEG, 100, fos);
		fos.flush();
		fos.close();
	}

	/**
	 * 从手机或者sd卡获取Bitmap
	 * @param fileName
	 * @return
	 */
	public Bitmap getBitmap(String fileName){
		return BitmapFactory.decodeFile(getStorageDirectory() + File.separator + fileName);
	}

	/**
	 * 判断文件是否存在
	 * @param fileName
	 * @return
	 */
	public boolean isFileExists(String fileName){
		return new File(getStorageDirectory() + File.separator + fileName).exists();
	}

	/**
	 * 获取文件的大小
	 * @param fileName
	 * @return
	 */
	public long getFileSize(String fileName) {
		return new File(getStorageDirectory() + File.separator + fileName).length();
	}

	/**
	 * 删除SD卡或者手机的缓存图片和目录
	 */
	public void deleteFile() {
		File dirFile = new File(getStorageDirectory());
		if(! dirFile.exists()){
			return;
		}
		if (dirFile.isDirectory()) {
			String[] children = dirFile.list();
			for (int i = 0; i < children.length; i++) {
				new File(dirFile, children[i]).delete();
			}
		}

		dirFile.delete();
	}
}

ImageDownLoader类,异步下载的核心类,保存图片到手机缓存,将图片加入LruCache中等等

package com.example.asyncimageloader;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Message;
import android.support.v4.util.LruCache;

public class ImageDownLoader {
	/**
	 * 缓存Image的类,当存储Image的大小大于LruCache设定的值,系统自动释放内存
	 */
	private LruCache<String, Bitmap> mMemoryCache;
	/**
	 * 操作文件相关类对象的引用
	 */
	private FileUtils fileUtils;
	/**
	 * 下载Image的线程池
	 */
	private ExecutorService mImageThreadPool = null;

	public ImageDownLoader(Context context){
		//获取系统分配给每个应用程序的最大内存,每个应用系统分配32M
		int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int mCacheSize = maxMemory / 8;
        //给LruCache分配1/8 4M
		mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){

			//必须重写此方法,来测量Bitmap的大小
			@Override
			protected int sizeOf(String key, Bitmap value) {
				return value.getRowBytes() * value.getHeight();
			}

		};

		fileUtils = new FileUtils(context);
	}

	/**
	 * 获取线程池的方法,因为涉及到并发的问题,我们加上同步锁
	 * @return
	 */
	public ExecutorService getThreadPool(){
		if(mImageThreadPool == null){
			synchronized(ExecutorService.class){
				if(mImageThreadPool == null){
					//为了下载图片更加的流畅,我们用了2个线程来下载图片
					mImageThreadPool = Executors.newFixedThreadPool(2);
				}
			}
		}

		return mImageThreadPool;

	}

	/**
	 * 添加Bitmap到内存缓存
	 * @param key
	 * @param bitmap
	 */
	public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
	    if (getBitmapFromMemCache(key) == null && bitmap != null) {
	        mMemoryCache.put(key, bitmap);
	    }
	}  

	/**
	 * 从内存缓存中获取一个Bitmap
	 * @param key
	 * @return
	 */
	public Bitmap getBitmapFromMemCache(String key) {
	    return mMemoryCache.get(key);
	} 

	/**
	 * 先从内存缓存中获取Bitmap,如果没有就从SD卡或者手机缓存中获取,SD卡或者手机缓存
	 * 没有就去下载
	 * @param url
	 * @param listener
	 * @return
	 */
	public Bitmap downloadImage(final String url, final onImageLoaderListener listener){
		//替换Url中非字母和非数字的字符,这里比较重要,因为我们用Url作为文件名,比如我们的Url
		//是Http://xiaanming/abc.jpg;用这个作为图片名称,系统会认为xiaanming为一个目录,
		//我们没有创建此目录保存文件就会报错
		final String subUrl = url.replaceAll("[^\\w]", "");
		Bitmap bitmap = showCacheBitmap(subUrl);
		if(bitmap != null){
			return bitmap;
		}else{

			final Handler handler = new Handler(){
				@Override
				public void handleMessage(Message msg) {
					super.handleMessage(msg);
					listener.onImageLoader((Bitmap)msg.obj, url);
				}
			};

			getThreadPool().execute(new Runnable() {

				@Override
				public void run() {
					Bitmap bitmap = getBitmapFormUrl(url);
					Message msg = handler.obtainMessage();
					msg.obj = bitmap;
					handler.sendMessage(msg);

					try {
						//保存在SD卡或者手机目录
						fileUtils.savaBitmap(subUrl, bitmap);
					} catch (IOException e) {
						e.printStackTrace();
					}

					//将Bitmap 加入内存缓存
					addBitmapToMemoryCache(subUrl, bitmap);
				}
			});
		}

		return null;
	}

	/**
	 * 获取Bitmap, 内存中没有就去手机或者sd卡中获取,这一步在getView中会调用,比较关键的一步
	 * @param url
	 * @return
	 */
	public Bitmap showCacheBitmap(String url){
		if(getBitmapFromMemCache(url) != null){
			return getBitmapFromMemCache(url);
		}else if(fileUtils.isFileExists(url) && fileUtils.getFileSize(url) != 0){
			//从SD卡获取手机里面获取Bitmap
			Bitmap bitmap = fileUtils.getBitmap(url);

			//将Bitmap 加入内存缓存
			addBitmapToMemoryCache(url, bitmap);
			return bitmap;
		}

		return null;
	}

	/**
	 * 从Url中获取Bitmap
	 * @param url
	 * @return
	 */
	private Bitmap getBitmapFormUrl(String url) {
		Bitmap bitmap = null;
		HttpURLConnection con = null;
		try {
			URL mImageUrl = new URL(url);
			con = (HttpURLConnection) mImageUrl.openConnection();
			con.setConnectTimeout(10 * 1000);
			con.setReadTimeout(10 * 1000);
			con.setDoInput(true);
			con.setDoOutput(true);
			bitmap = BitmapFactory.decodeStream(con.getInputStream());
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (con != null) {
				con.disconnect();
			}
		}
		return bitmap;
	}

	/**
	 * 取消正在下载的任务
	 */
	public synchronized void cancelTask() {
		if(mImageThreadPool != null){
			mImageThreadPool.shutdownNow();
			mImageThreadPool = null;
		}
	}

	/**
	 * 异步下载图片的回调接口
	 * @author len
	 *
	 */
	public interface onImageLoaderListener{
		void onImageLoader(Bitmap bitmap, String url);
	}

}

ImageDownLoader中有几个方法比较重要

  1. 首先我们需要重写sizeOf(String key, Bitmap value)来计算图片的大小,默认返回图片的数量
  2. downloadImage(final String url, final onImageLoaderListener listener)先去LruCache查看Image,没有再去手机缓存中查看,在没有则开启线程下载,这里我们提供了一个回调接口,回调方法中我们将Bitmap和图片Url作为参数,String subUrl = url.replaceAll("[^\\w]", "") 我在代码中注释写的比较清楚
  3. showCacheBitmap(String url)方法,此方法在Adapter中的getView()当中调用,如果getView()中不调用此方法试试你就知道效果了
  • ImageAdapter  GridView的适配器类,主要是GridView滑动的时候取消下载任务,静止的时候去下载当前显示的item的图片,其他也没什么不同了
package com.example.asyncimageloader;

import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import com.example.asyncimageloader.ImageDownLoader.onImageLoaderListener;

public class ImageAdapter extends BaseAdapter implements OnScrollListener{
	/**
	 * 上下文对象的引用
	 */
	private Context context;

	/**
	 * Image Url的数组
	 */
	private String [] imageThumbUrls;

	/**
	 * GridView对象的应用
	 */
	private GridView mGridView;

	/**
	 * Image 下载器
	 */
	private ImageDownLoader mImageDownLoader;

	/**
	 * 记录是否刚打开程序,用于解决进入程序不滚动屏幕,不会下载图片的问题。
	 * 参考http://blog.csdn.net/guolin_blog/article/details/9526203#comments
	 */
	private boolean isFirstEnter = true;

	/**
	 * 一屏中第一个item的位置
	 */
	private int mFirstVisibleItem;

	/**
	 * 一屏中所有item的个数
	 */
	private int mVisibleItemCount;

	public ImageAdapter(Context context, GridView mGridView, String [] imageThumbUrls){
		this.context = context;
		this.mGridView = mGridView;
		this.imageThumbUrls = imageThumbUrls;
		mImageDownLoader = new ImageDownLoader(context);
		mGridView.setOnScrollListener(this);
	}

	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		//仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务
		if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE){
			showImage(mFirstVisibleItem, mVisibleItemCount);
		}else{
			cancelTask();
		}

	}

	/**
	 * GridView滚动的时候调用的方法,刚开始显示GridView也会调用此方法
	 */
	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		mFirstVisibleItem = firstVisibleItem;
		mVisibleItemCount = visibleItemCount;
		// 因此在这里为首次进入程序开启下载任务。
		if(isFirstEnter && visibleItemCount > 0){
			showImage(mFirstVisibleItem, mVisibleItemCount);
			isFirstEnter = false;
		}
	}

	@Override
	public int getCount() {
		return imageThumbUrls.length;
	}

	@Override
	public Object getItem(int position) {
		return imageThumbUrls[position];
	}

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

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ImageView mImageView;
		final String mImageUrl = imageThumbUrls[position];
		if(convertView == null){
			mImageView = new ImageView(context);
		}else{
			mImageView = (ImageView) convertView;
		}

		mImageView.setLayoutParams(new GridView.LayoutParams(150, 150));
		mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);

		//给ImageView设置Tag,这里已经是司空见惯了
		mImageView.setTag(mImageUrl);

		/*******************************去掉下面这几行试试是什么效果****************************/
		Bitmap bitmap = mImageDownLoader.showCacheBitmap(mImageUrl.replaceAll("[^\\w]", ""));
		if(bitmap != null){
			mImageView.setImageBitmap(bitmap);
		}else{
			mImageView.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_empty));
		}
		/**********************************************************************************/

		return mImageView;
	}

	/**
	 * 显示当前屏幕的图片,先会去查找LruCache,LruCache没有就去sd卡或者手机目录查找,在没有就开启线程去下载
	 * @param firstVisibleItem
	 * @param visibleItemCount
	 */
	private void showImage(int firstVisibleItem, int visibleItemCount){
		Bitmap bitmap = null;
		for(int i=firstVisibleItem; i<firstVisibleItem + visibleItemCount; i++){
			String mImageUrl = imageThumbUrls[i];
			final ImageView mImageView = (ImageView) mGridView.findViewWithTag(mImageUrl);
			bitmap = mImageDownLoader.downloadImage(mImageUrl, new onImageLoaderListener() {

				@Override
				public void onImageLoader(Bitmap bitmap, String url) {
					if(mImageView != null && bitmap != null){
						mImageView.setImageBitmap(bitmap);
					}

				}
			});

			//if(bitmap != null){
			//	mImageView.setImageBitmap(bitmap);
			//}else{
			//	mImageView.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_empty));
			//}
		}
	}

	/**
	 * 取消下载任务
	 */
	public void cancelTask(){
		mImageDownLoader.cancelTask();
	}

}

MainActivity  里面一个GridView,然后提供一个系统菜单来删除手机上的缓存图片,直接上代码,比较简单所以里面也没有注释

package com.example.asyncimageloader;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.GridView;
import android.widget.Toast;

public class MainActivity extends Activity {
	private GridView mGridView;
	private String [] imageThumbUrls = Images.imageThumbUrls;
	private ImageAdapter mImageAdapter;
	private FileUtils fileUtils;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		fileUtils = new FileUtils(this);
		mGridView = (GridView) findViewById(R.id.gridView);
		mImageAdapter = new ImageAdapter(this, mGridView, imageThumbUrls);
		mGridView.setAdapter(mImageAdapter);
	}

	@Override
	protected void onDestroy() {
		mImageAdapter.cancelTask();
		super.onDestroy();
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		super.onCreateOptionsMenu(menu);
		menu.add("删除手机中图片缓存");
		return super.onCreateOptionsMenu(menu);
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case 0:
			fileUtils.deleteFile();
			Toast.makeText(getApplication(), "清空缓存成功", Toast.LENGTH_SHORT).show();
			break;
		}
		return super.onOptionsItemSelected(item);
	}

}

上面的代码比较完善也比较复杂,我们再来看一下别人的思路:基本思路也是内存缓存加文件缓存,内存缓存使用了LruCache<String, Bitmap>来让虚拟机自己管理,文件缓存则在写入文件时进行了压缩bitmap.compress(CompressFormat.JPEG, 100, fos),这都是比较好的思路。另外异步线程使用了线程池,加了锁,这样会使每次只能开两个线程去加载图片,这样确实避免了重复开启线程去加载图片的问题,但是效率也比较低(指下载效率)。另外图片下载完毕以后,也是交给handler去处理,从而更新UI。

在图片写入本地时也注意到了要替换图片路径中的非字母数字符号的问题。

另外给adapter添加了ScrollListener接口,在onScrollStateChanged(AbsListView view, int scrollState)方法中,判断当前滑动状态,如果状态为AbsListView.OnScrollListener.SCROLL_STATE_IDLE(也就是静止时),采取加载图片,否则取消所有下载线程。

在onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount)方法中,获取当前第一条可见的item的id,和可见item总数目,从而在下载时只下载看见item对应的图片。

使用imageview.gettag()方法避免图片错位的问题。

缺点是效率问题,每次最多有两个线程去下载图片,其他下载线程阻塞,另外没有对图片缩放进行处理,但是思路基本完善。

下面来看第四个例子

public class PhotoWallAdapter extends ArrayAdapter<String> {

	/**
	 * 记录所有正在下载或等待下载的任务。
	 */
	private Set<BitmapWorkerTask> taskCollection;

	/**
	 * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
	 */
	private LruCache<String, Bitmap> mMemoryCache;

	/**
	 * 图片硬盘缓存核心类。
	 */
	private DiskLruCache mDiskLruCache;

	/**
	 * GridView的实例
	 */
	private GridView mPhotoWall;

	/**
	 * 记录每个子项的高度。
	 */
	private int mItemHeight = 0;

	public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects,
			GridView photoWall) {
		super(context, textViewResourceId, objects);
		mPhotoWall = photoWall;
		taskCollection = new HashSet<BitmapWorkerTask>();
		// 获取应用程序最大可用内存
		int maxMemory = (int) Runtime.getRuntime().maxMemory();
		int cacheSize = maxMemory / 8;
		// 设置图片缓存大小为程序最大可用内存的1/8
		mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
			@Override
			protected int sizeOf(String key, Bitmap bitmap) {
				return bitmap.getByteCount();
			}
		};
		try {
			// 获取图片缓存路径
			File cacheDir = getDiskCacheDir(context, "thumb");
			if (!cacheDir.exists()) {
				cacheDir.mkdirs();
			}
			// 创建DiskLruCache实例,初始化缓存数据
			mDiskLruCache = DiskLruCache
					.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		final String url = getItem(position);
		View view;
		if (convertView == null) {
			view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);
		} else {
			view = convertView;
		}
		final ImageView imageView = (ImageView) view.findViewById(R.id.photo);
		if (imageView.getLayoutParams().height != mItemHeight) {
			imageView.getLayoutParams().height = mItemHeight;
		}
		// 给ImageView设置一个Tag,保证异步加载图片时不会乱序
		imageView.setTag(url);
		imageView.setImageResource(R.drawable.empty_photo);
		loadBitmaps(imageView, url);
		return view;
	}

	/**
	 * 将一张图片存储到LruCache中。
	 *
	 * @param key
	 *            LruCache的键,这里传入图片的URL地址。
	 * @param bitmap
	 *            LruCache的键,这里传入从网络上下载的Bitmap对象。
	 */
	public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
		if (getBitmapFromMemoryCache(key) == null) {
			mMemoryCache.put(key, bitmap);
		}
	}

	/**
	 * 从LruCache中获取一张图片,如果不存在就返回null。
	 *
	 * @param key
	 *            LruCache的键,这里传入图片的URL地址。
	 * @return 对应传入键的Bitmap对象,或者null。
	 */
	public Bitmap getBitmapFromMemoryCache(String key) {
		return mMemoryCache.get(key);
	}

	/**
	 * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageView的Bitmap对象,
	 * 如果发现任何一个ImageView的Bitmap对象不在缓存中,就会开启异步线程去下载图片。
	 */
	public void loadBitmaps(ImageView imageView, String imageUrl) {
		try {
			Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
			if (bitmap == null) {
				BitmapWorkerTask task = new BitmapWorkerTask();
				taskCollection.add(task);
				task.execute(imageUrl);
			} else {
				if (imageView != null && bitmap != null) {
					imageView.setImageBitmap(bitmap);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 取消所有正在下载或等待下载的任务。
	 */
	public void cancelAllTasks() {
		if (taskCollection != null) {
			for (BitmapWorkerTask task : taskCollection) {
				task.cancel(false);
			}
		}
	}

	/**
	 * 根据传入的uniqueName获取硬盘缓存的路径地址。
	 */
	public File getDiskCacheDir(Context context, String uniqueName) {
		String cachePath;
		if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
				|| !Environment.isExternalStorageRemovable()) {
			cachePath = context.getExternalCacheDir().getPath();
		} else {
			cachePath = context.getCacheDir().getPath();
		}
		return new File(cachePath + File.separator + uniqueName);
	}

	/**
	 * 获取当前应用程序的版本号。
	 */
	public int getAppVersion(Context context) {
		try {
			PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(),
					0);
			return info.versionCode;
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}
		return 1;
	}

	/**
	 * 设置item子项的高度。
	 */
	public void setItemHeight(int height) {
		if (height == mItemHeight) {
			return;
		}
		mItemHeight = height;
		notifyDataSetChanged();
	}

	/**
	 * 使用MD5算法对传入的key进行加密并返回。
	 */
	public String hashKeyForDisk(String key) {
		String cacheKey;
		try {
			final MessageDigest mDigest = MessageDigest.getInstance("MD5");
			mDigest.update(key.getBytes());
			cacheKey = bytesToHexString(mDigest.digest());
		} catch (NoSuchAlgorithmException e) {
			cacheKey = String.valueOf(key.hashCode());
		}
		return cacheKey;
	}

	/**
	 * 将缓存记录同步到journal文件中。
	 */
	public void fluchCache() {
		if (mDiskLruCache != null) {
			try {
				mDiskLruCache.flush();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private String bytesToHexString(byte[] bytes) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < bytes.length; i++) {
			String hex = Integer.toHexString(0xFF & bytes[i]);
			if (hex.length() == 1) {
				sb.append('0');
			}
			sb.append(hex);
		}
		return sb.toString();
	}

	/**
	 * 异步下载图片的任务。
	 *
	 * @author guolin
	 */
	class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {

		/**
		 * 图片的URL地址
		 */
		private String imageUrl;

		@Override
		protected Bitmap doInBackground(String... params) {
			imageUrl = params[0];
			FileDescriptor fileDescriptor = null;
			FileInputStream fileInputStream = null;
			Snapshot snapShot = null;
			try {
				// 生成图片URL对应的key
				final String key = hashKeyForDisk(imageUrl);
				// 查找key对应的缓存
				snapShot = mDiskLruCache.get(key);
				if (snapShot == null) {
					// 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
					DiskLruCache.Editor editor = mDiskLruCache.edit(key);
					if (editor != null) {
						OutputStream outputStream = editor.newOutputStream(0);
						if (downloadUrlToStream(imageUrl, outputStream)) {
							editor.commit();
						} else {
							editor.abort();
						}
					}
					// 缓存被写入后,再次查找key对应的缓存
					snapShot = mDiskLruCache.get(key);
				}
				if (snapShot != null) {
					fileInputStream = (FileInputStream) snapShot.getInputStream(0);
					fileDescriptor = fileInputStream.getFD();
				}
				// 将缓存数据解析成Bitmap对象
				Bitmap bitmap = null;
				if (fileDescriptor != null) {
					bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
				}
				if (bitmap != null) {
					// 将Bitmap对象添加到内存缓存当中
					addBitmapToMemoryCache(params[0], bitmap);
				}
				return bitmap;
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				if (fileDescriptor == null && fileInputStream != null) {
					try {
						fileInputStream.close();
					} catch (IOException e) {
					}
				}
			}
			return null;
		}

		@Override
		protected void onPostExecute(Bitmap bitmap) {
			super.onPostExecute(bitmap);
			// 根据Tag找到相应的ImageView控件,将下载好的图片显示出来。
			ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);
			if (imageView != null && bitmap != null) {
				imageView.setImageBitmap(bitmap);
			}
			taskCollection.remove(this);
		}

		/**
		 * 建立HTTP请求,并获取Bitmap对象。
		 *
		 * @param imageUrl
		 *            图片的URL地址
		 * @return 解析后的Bitmap对象
		 */
		private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
			HttpURLConnection urlConnection = null;
			BufferedOutputStream out = null;
			BufferedInputStream in = null;
			try {
				final URL url = new URL(urlString);
				urlConnection = (HttpURLConnection) url.openConnection();
				in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
				out = new BufferedOutputStream(outputStream, 8 * 1024);
				int b;
				while ((b = in.read()) != -1) {
					out.write(b);
				}
				return true;
			} catch (final IOException e) {
				e.printStackTrace();
			} finally {
				if (urlConnection != null) {
					urlConnection.disconnect();
				}
				try {
					if (out != null) {
						out.close();
					}
					if (in != null) {
						in.close();
					}
				} catch (final IOException e) {
					e.printStackTrace();
				}
			}
			return false;
		}

	}

}

思路与前一个例子相识,这里的重点是使用了DiskLrucache类来做文件缓存。

缺点较多,例如也会出现多个线程下载同一图片的问题,没有监听滑动状况。

看了上面的所有例子,我们现在来总结最佳优化方法的具体实现。首先上面所有的例子都没有处理图片的缩放问题。

可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

那我们怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:

public static int calculateInSampleSize(BitmapFactory.Options options,
		int reqWidth, int reqHeight) {
	// 源图片的高度和宽度
	final int height = options.outHeight;
	final int width = options.outWidth;
	int inSampleSize = 1;
	if (height > reqHeight || width > reqWidth) {
		// 计算出实际宽高和目标宽高的比率
		final int heightRatio = Math.round((float) height / (float) reqHeight);
		final int widthRatio = Math.round((float) width / (float) reqWidth);
		// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
		// 一定都会大于等于目标的宽和高。
		inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
	}
	return inSampleSize;
}

使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
	// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 调用上面定义的方法计算inSampleSize值
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 使用获取到的inSampleSize值再次解析图片
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

下面的代码非常简单地将任意一张图片压缩成100*100的缩略图,并在ImageView上展示。

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

也就说缩放是图片处理的第一步。

接着就是使用Lrucache,DiskLruCache来分别进行内存缓存和硬盘缓存。

如果缓存没有,开启线程去下载,我们要把每个线程保存在一个Map中,防止出现多个线程下载一张图片的线程(上面的所有例子都没有做到这一点)。另外也可以使用线程池,但是适当的将线程池的容量设置大一些,避免效率问题。

最后Adapter要继承OnScrollListener接口,在静止时才加载图片,否则取消所有下载线程。

使用Imageview.getTag()方法防止图片错位。

OK,到此为止,每个优化点的具体实现步骤我都说清楚,虽然有点复杂,但是大家思路正确,就一定可以写好。

时间: 2024-10-28 11:34:45

Listview的使用与优化(下)的相关文章

Listview的使用与优化(中)

上篇文章简单地介绍了listview的使用和优化,都是一些常见的优化技巧.但是listview优化还有一些重要的问题,那就是图片加载,异步加载的优化,因为图片占用内存较大,listview在滑动过程中很容易产生OOM的现象,下面我来给大家解释一下图片异步加载的优化思路. 总的来说有一下几个优化思路: 1,对Imageview使用setTag()方法来解决图片错位问题,这个Tag中设置的是图片的url,然后在加载的时候取得这个url和要加载那position中的url对比,如果不相同就加载,相同就

ListView上拉加载下拉刷新

主要用到了这个几个文件,MainActivity是界面的Activity,MyAdapter是ListView的自定义适配,MyListView是自定义带头部LIistView,如果只需要上拉加载就不需要:activity_main.xml是住界面,item.xml是ListView的子布局里面只有一个TextView,listview_footer.xml是listview的加载更多的底部布局,listview_header.xml是listview的头部布局. MainActivity.ja

JSP页面小脚本实现日期比较,Java同理,精简过后的,可能在效率上不太好,有大大可以给优化下就更好了

<% java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat("yyyy-MM-dd hh-mm-ss"); java.util.Date d = formatter.parse("2014-06-18 07-30-00"); if (java.lang.System.currentTimeMillis() > d.getTime()) { %> 111111111

【Android笔记】listview加载性能优化及有多种listitem布局处理方式

在android开发中Listview是一个很重要的组件,它以列表的形式根据数据的长自适应展示具体内容. 用户可以自由的定义listview每一列的布局,但当listview有大量的数据需要加载的时候,会占据大量内存,影响性能,这时候就需要按需填充并重新使用view来减少对象的创建. ListView加载数据都是在 1 public View getView(int position, View convertView, ViewGroup parent) { 2 3 ...... 4 5 }

安卓开发笔记——ListView加载性能优化ViewHolder

在前不久做安卓项目的时候,其中有个功能是爬取某网站上的新闻信息,用ListView展示,虽然做了分页,但还是觉得达不到理想流畅效果. 上网查阅了些资料,发现一些挺不错的总结,这里记录下,便于复习. 当ListView有大量的数据需要加载的时候,会占据大量内存,影响性能. 经过测试,发现耗费大量资源是在ListView去加载布局文件的时候,也就是findViewById的时候,这时我们就该考虑如何复用这个布局文件对象,以减少对象的创建. ListView加载数据是在public View getV

关于ListView基本使用与优化(转)

我们经常会在应用程序中使用列表的形式来展现一些内容,所以学好ListView是非常必需的.ListView也是Android中比较难以使用的控件,这节内容就将详细解读ListView的用法. 一个ListView通常有两个职责. (1)将数据填充到布局. (2)处理用户的选择点击等操作. 第一点很好理解,ListView就是实现这个功能的.第二点也不难做到,在后面的学习中读者会发现,这非常简单. 一个ListView的创建需要3个元素. (1)ListView中的每一列的View. (2)填入V

【转】关于大型网站技术演进的思考(二十一)--网站静态化处理—web前端优化—下【终篇】(13)

本篇继续web前端优化的讨论,开始我先讲个我所知道的一个故事,有家大型的企业顺应时代发展的潮流开始投身于互联网行业了,它们为此专门设立了一个事业部,不过该企业把这个事业部里的人事成本,系统运维成本特别是硬件采购的成本都由总公司来承担,当然互联网业务上的市场营销成本这块还是由该事业部自己承担,可是网站一年运维下来,该公司发现该事业部里最大的成本居然不是市场营销的开销,而是短信业务和宽带使用上的开销,是不是有点让人感到意外呢?下面我来分析下这个场景吧. 短信这块是和通讯运营商有关,很难从根本上解决,

关于大型网站技术演进的思考(二十一)--网站静态化处理—web前端优化—下【终篇】(13)

本篇继续web前端优化的讨论,开始我先讲个我所知道的一个故事,有家大型的企业顺应时代发展的潮流开始投身于互联网行业了,它们为此专门设立了一个事业部,不过该企业把这个事业部里的人事成本,系统运维成本特别是硬件采购的成本都由总公司来承担,当然互联网业务上的市场营销成本这块还是由该事业部自己承担,可是网站一年运维下来,该公司发现该事业部里最大的成本居然不是市场营销的开销,而是短信业务和宽带使用上的开销,是不是有点让人感到意外呢?下面我来分析下这个场景吧. 短信这块是和通讯运营商有关,很难从根本上解决,

Android开发之ListView中Adapter的优化

ListView是Android开发最常用的控件,适配器adapter是将要显示的数据映射到View中并添加到ListView中显示 在实现ListView时,我们需要定义适配器如BaseAdapter.ArrayAdapter.CursorAdapter.SimpleAdapter等,并且重写其一下四个方法: 1 public int getCount(): 2 public Object getItem(int position) 3  public long getItemId(int p