高效加载本地相册图片的ImageLoader类

当我们相册中的图片有几千张的时候,你快速的拖动滚动条到底部,怎么样才能保证图片加载的流畅性以及避免OOM呢

         1.使用Lru算法对图片进行缓存保证流畅性以及避免OOM

         2.图片加载肯定是要异步进行的,那么就涉及到多线程的并发进行,使用线程池对任务进行调度

         3.使用android内部的异步消息机制Looper+Handler对taskQueue进行轮询执行

1.图片加载器,task任务列频繁调用,所以要做成单例模式,编写一个单例的ImageLoader类,并声明必要的成员变量

/**
 * @author Administrator
 *	图片加载器,task任务列频繁调用,所以要做成单例模式
 */
public class ImageLoader {
	private static ImageLoader instance;
	/**
	 * 图片缓存的核心对象
	 */
	private LruCache mLruCache;
	/**
	 * 线程池
	 */
	private ExecutorService mThreadPool;
	private static final int DEFAULT_THREAD_COUNT=1;
	/**
	 * 队列调度方式
	 */
	private static Type mType=Type.LIFO;
	/**
	 * 任务队列
	 */
	private LinkedList mTaskQueue;
	/**
	 * 后台轮询线程
	 */
	private Thread mPoolThread;
	private Handler mPoolThreadHandler;
	/**
	 *UI线程的Handler
	 */
	private Handler mUIHandler;
	//任务队列的
	public enum Type{
		FIFO,LIFO;
	}
	/**
	 * 获取ImageLoader的单利方法
	 * @return
	 */
	public static ImageLoader getInstance(){
		//这里采用双重if判断,可以提高代码的执行效率
		//外层判断可能会有几个线程进到里面,然后线程同步加条件判断
		//保证单例唯一性
		if(instance==null){
			synchronized (ImageLoader.class) {
				if(instance==null){
					instance=new ImageLoader(DEFAULT_THREAD_COUNT,mType);
				}
			}
		}
		return instance;
	}

2.初始化成员变量

private ImageLoader(int threadCount,Type type){
		init(threadCount,type);
	}
	private Semaphore mSemaphorePoolThreadHandler=new Semaphore(0);
	private Semaphore mSemaphoreThreadPool;
	/**
	 * 构造方法中初始化一大堆成员变量
	 * @param threadCount
	 * @param type
	 */
	private void init(int threadCount,Type type) {
		//初始化后台轮询线程
		mPoolThread=new Thread(){
			@Override
			public void run() {
				Looper.prepare();//使用安卓异步消息机制处理mtaskQueue中的任务
				mPoolThreadHandler=new Handler(){
					@Override
					public void handleMessage(Message msg) {
						//从线程池取出一个任务执行
						mThreadPool.execute(getTask());
						
					}
				};
				mSemaphorePoolThreadHandler.release();//handler初始化完成,添加一个许可证
				Looper.loop();
			}
		};
		//启动线程池
		mPoolThread.start();
		mTaskQueue=new LinkedList();//任务队列
		mThreadPool=Executors.newFixedThreadPool(threadCount);//线程池管理者
		int maxSize=(int) Runtime.getRuntime().maxMemory();
		int cacheSize=maxSize/8;
		mLruCache=new LruCache(cacheSize){

			@Override
			protected int sizeOf(String key, Bitmap value) {
				return value.getRowBytes()*value.getHeight();
			}
			
		};
		mType=type;
		mSemaphoreThreadPool=new Semaphore(threadCount);		
	}

3.编写和新方法loadImage()

/**
	 * 图片加载的核心方法
	 * @param path
	 * @param imageView
	 */
	public void loadImage(final String path,final ImageView imageView){
		imageView.setTag(path);//带上标记,防止条目复用时出现乱跳现象
		mUIHandler=new Handler(){
			@Override
			public void handleMessage(Message msg) {
				ImageHolder holder=(ImageHolder) msg.obj;
				Bitmap bitmap=holder.mBitmap;
				ImageView imageView=holder.mImageView;
				String url=holder.path;
				if(imageView.getTag().toString().equals(url)){
					imageView.setImageBitmap(bitmap);
				}
			}
		};
		
		Bitmap mBitmap=getBitmapFromLruCache(path);
		
		if(mBitmap!=null){
			refreshHandler(path, imageView, mBitmap);
		}
		else{
			addTask(imageView,path);
		}
		
	}
	private void addTask(final ImageView imageView,final String path) {
		mTaskQueue.add(new Runnable() {
			
			@Override
			public void run() {
				//进行加载图片的逻辑
				//1.获取ImageView要显示的大小
				ImageSize imageSize = getImageSize(imageView);
				//2.开始压缩图片
				Bitmap mBitmap = compressBitmap(imageSize,path);
				//3.把图片添加到LruCache中去
				if(mLruCache.get(path)!=null){
					mLruCache.put(path, mBitmap);
				}
				//发送消息,让UiHandler把图片显示到imageview上去
				refreshHandler(path, imageView, mBitmap);
				//任务完成,可以释放许可,让后台轮询线程去处理下一个任务
				mSemaphoreThreadPool.release();
			}
		});
		try {
			if(mPoolThreadHandler==null){
				mSemaphorePoolThreadHandler.acquire();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//发送消息,让后台轮询线程去处理任务堆
		mPoolThreadHandler.sendEmptyMessage(0X110);
		
	}
	/**
	 * 把ImageView path Bitmap等信息发送到handler中去执行
	 * @param path
	 * @param imageView
	 * @param mBitmap
	 */
	private void refreshHandler(final String path, final ImageView imageView, Bitmap mBitmap) {
		Message message=Message.obtain();
		ImageHolder holder=new ImageHolder();
		holder.mBitmap=mBitmap;
		holder.mImageView=imageView;
		holder.path=path;
		message.obj=holder;
		mUIHandler.sendMessage(message);
	}
	/**
	 * 压缩图片
	 * @return
	 */
	private Bitmap compressBitmap(ImageSize imageSize,String path){
		BitmapFactory.Options option=new BitmapFactory.Options();
		option.inJustDecodeBounds=true;//只解析图片的宽高等信息,而不把图片加载到内存中去
		BitmapFactory.decodeFile(path, option);
		int outWidth = option.outWidth;
		int outHeight = option.outHeight;
		int width=imageSize.width;
		int height=imageSize.height;
		int inSimpleSize=1;
		if(outHeight>height||outWidth>height){
			int widthRadio=Math.round(outWidth*1.0f/width);
			int heightRadio=Math.round(outHeight*1.0f/height);
			inSimpleSize=Math.max(widthRadio, heightRadio);
		}
		option.inJustDecodeBounds=false;
		option.inSampleSize=inSimpleSize;
		
		return BitmapFactory.decodeFile(path, option);
	}
	/**
	 * 根据ImageView获取适当的压缩后的宽和高
	 * @param iv
	 * @return
	 */
	private ImageSize getImageSize(ImageView iv){
		LayoutParams lp = iv.getLayoutParams();
		//获取屏幕的矩阵
		DisplayMetrics dm = iv.getContext().getResources().getDisplayMetrics();
		int width=iv.getWidth();
		if(width<=0){
			width=lp.width;
		}
		if(width<=0){
			//使用反射获取ImageView的MaxWidth和MaxHeight兼容低版本
			width=getMaxValueFromInvoke(iv,"mMaxWidth");
		}
		if(width<=0){
			width=dm.widthPixels;
		}
		
		int height=iv.getHeight();
		if(height<=0){
			height=lp.width;
		}
		if(height<=0){
			height=getMaxValueFromInvoke(iv,"mMaxHeight");
		}
		if(height<=0){
			height=dm.heightPixels;
		}
		return new ImageSize(width, height);
	}
	/**
	 * 使用反射获取ImageView的MaxWidth和MaxHeight兼容低版本
	 * @param obj
	 * @param fieldName
	 * @return
	 */
	private int getMaxValueFromInvoke(Object obj,String fieldName){
		int value=0;
		try {
			Field field = ImageView.class.getDeclaredField(fieldName);
			field.setAccessible(true);
			int fieldValue = field.getInt(obj);
			if(fieldValue>0&&fieldValue

获取ImageView的最大宽高的时候为了兼容低版本,没使用getMaxWidth()这个api,而是采用了反射获取到的

其中后台轮询线程还没初始化mThreadPoolHandler的时候,mThreadPoolHandler就可能在loadImage()方法中被调用了,所以就存在空指针的风险

为了保证并发危险,采用java提供的一个并发信号量的类Semaphore   初始化的时候在构造方法中指定有几个许可证

有两个方法:

          1.acquire()每执行一次消耗一个许可证,但许可证为0的时候,阻塞线程,直到Semaphore   再次调用release()获取到许可证,结束阻塞

           2.release()每执行一次获取一个许可证

下面我再把整个类的代码全部粘过来

package com.example.fandayimageloader.imageloader;

import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v4.util.LruCache;
import android.util.DisplayMetrics;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
/**
 * 这个线程中的mPoolThreadHandler可能还没有初始化完成,但是在addTask到mTaskQueue后就要用到时,就会出现null painter exception
 * 解决这个可以使用java提供的Semaphore,并发一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),
 * 然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,
 * 并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()和release()获取和释放访问许可。
 * 简单来讲就是acquire()消耗掉一个许可,而release()添加一个许可,当许可为0的时候,想要再次acquire()就会阻塞
 * public class SemaphoreTest {
 *
 *	     public static void main(String[] args) {  
 *	        // 线程池 
 *	        ExecutorService exec = Executors.newCachedThreadPool();  
 *	        // 只能5个线程同时访问 
 *	        final Semaphore semp = new Semaphore(5);  
 *	        // 模拟20个客户端访问 
 *	        for (int index = 0; index < 20; index++) {
 *	            final int NO = index;  
 *	            Runnable run = new Runnable() {  
 *	                public void run() {  
 *	                    try {  
 * 	                        // 获取许可 
 *	                        semp.acquire();  
 *                       System.out.println("Accessing: " + NO);  
 *	                        Thread.sleep((long) (Math.random() * 10000));  
 *	                        // 访问完后,释放 ,如果屏蔽下面的语句,则在控制台只能打印5条记录,之后线程一直阻塞
 *	                        semp.release();  
 *	                    } catch (InterruptedException e) {  
 *	                    }  
 *	                }  
 *	            };  
 *	            exec.execute(run);  
 *	        }  
 *	        // 退出线程池 
 *	        exec.shutdown();  
 *	    }  
 *	}
 */

/**
 * @author Administrator
 *	图片加载器,task任务列频繁调用,所以要做成单例模式
 */
public class ImageLoader {
	private static ImageLoader instance;
	/**
	 * 图片缓存的核心对象
	 */
	private LruCache mLruCache;
	/**
	 * 线程池
	 */
	private ExecutorService mThreadPool;
	private static final int DEFAULT_THREAD_COUNT=1;
	/**
	 * 队列调度方式
	 */
	private static Type mType=Type.LIFO;
	/**
	 * 任务队列
	 */
	private LinkedList mTaskQueue;
	/**
	 * 后台轮询线程
	 */
	private Thread mPoolThread;
	private Handler mPoolThreadHandler;
	/**
	 *UI线程的Handler
	 */
	private Handler mUIHandler;
	//任务队列的
	public enum Type{
		FIFO,LIFO;
	}
	/**
	 * 获取ImageLoader的单利方法
	 * @return
	 */
	public static ImageLoader getInstance(){
		//这里采用双重if判断,可以提高代码的执行效率
		//外层判断可能会有几个线程进到里面,然后线程同步加条件判断
		//保证单例唯一性
		if(instance==null){
			synchronized (ImageLoader.class) {
				if(instance==null){
					instance=new ImageLoader(DEFAULT_THREAD_COUNT,mType);
				}
			}
		}
		return instance;
	}
	
	private ImageLoader(int threadCount,Type type){
		init(threadCount,type);
	}
	private Semaphore mSemaphorePoolThreadHandler=new Semaphore(0);
	private Semaphore mSemaphoreThreadPool;
	/**
	 * 构造方法中初始化一大堆成员变量
	 * @param threadCount
	 * @param type
	 */
	private void init(int threadCount,Type type) {
		//初始化后台轮询线程
		mPoolThread=new Thread(){
			@Override
			public void run() {
				Looper.prepare();//使用安卓异步消息机制处理mtaskQueue中的任务
				mPoolThreadHandler=new Handler(){
					@Override
					public void handleMessage(Message msg) {
						//从线程池取出一个任务执行
						mThreadPool.execute(getTask());
						
					}
				};
				mSemaphorePoolThreadHandler.release();//handler初始化完成,添加一个许可证
				Looper.loop();
			}
		};
		//启动线程池
		mPoolThread.start();
		mTaskQueue=new LinkedList();//任务队列
		mThreadPool=Executors.newFixedThreadPool(threadCount);//线程池管理者
		int maxSize=(int) Runtime.getRuntime().maxMemory();
		int cacheSize=maxSize/8;
		mLruCache=new LruCache(cacheSize){

			@Override
			protected int sizeOf(String key, Bitmap value) {
				return value.getRowBytes()*value.getHeight();
			}
			
		};
		mType=type;
		mSemaphoreThreadPool=new Semaphore(threadCount);		
	}
	/**
	 * 从任务队列中根据Type的值取出任务
	 * @return
	 */
	public Runnable getTask(){
		try {
			//每取一个任务就消耗一个许可,消耗完成将阻塞,等待释放许可
			mSemaphoreThreadPool.acquire();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if(mType==Type.FIFO){
			return mTaskQueue.removeFirst();
		}
		else if(mType==Type.LIFO){
			return mTaskQueue.removeLast();
		}
		return null;
	}
	
	/**
	 * 图片加载的核心方法
	 * @param path
	 * @param imageView
	 */
	public void loadImage(final String path,final ImageView imageView){
		imageView.setTag(path);//带上标记,防止条目复用时出现乱跳现象
		mUIHandler=new Handler(){
			@Override
			public void handleMessage(Message msg) {
				ImageHolder holder=(ImageHolder) msg.obj;
				Bitmap bitmap=holder.mBitmap;
				ImageView imageView=holder.mImageView;
				String url=holder.path;
				if(imageView.getTag().toString().equals(url)){
					imageView.setImageBitmap(bitmap);
				}
			}
		};
		
		Bitmap mBitmap=getBitmapFromLruCache(path);
		
		if(mBitmap!=null){
			refreshHandler(path, imageView, mBitmap);
		}
		else{
			addTask(imageView,path);
		}
		
	}
	private void addTask(final ImageView imageView,final String path) {
		mTaskQueue.add(new Runnable() {
			
			@Override
			public void run() {
				//进行加载图片的逻辑
				//1.获取ImageView要显示的大小
				ImageSize imageSize = getImageSize(imageView);
				//2.开始压缩图片
				Bitmap mBitmap = compressBitmap(imageSize,path);
				//3.把图片添加到LruCache中去
				if(mLruCache.get(path)!=null){
					mLruCache.put(path, mBitmap);
				}
				//发送消息,让UiHandler把图片显示到imageview上去
				refreshHandler(path, imageView, mBitmap);
				//任务完成,可以释放许可,让后台轮询线程去处理下一个任务
				mSemaphoreThreadPool.release();
			}
		});
		try {
			if(mPoolThreadHandler==null){
				mSemaphorePoolThreadHandler.acquire();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//发送消息,让后台轮询线程去处理任务堆
		mPoolThreadHandler.sendEmptyMessage(0X110);
		
	}
	/**
	 * 把ImageView path Bitmap等信息发送到handler中去执行
	 * @param path
	 * @param imageView
	 * @param mBitmap
	 */
	private void refreshHandler(final String path, final ImageView imageView, Bitmap mBitmap) {
		Message message=Message.obtain();
		ImageHolder holder=new ImageHolder();
		holder.mBitmap=mBitmap;
		holder.mImageView=imageView;
		holder.path=path;
		message.obj=holder;
		mUIHandler.sendMessage(message);
	}
	/**
	 * 压缩图片
	 * @return
	 */
	private Bitmap compressBitmap(ImageSize imageSize,String path){
		BitmapFactory.Options option=new BitmapFactory.Options();
		option.inJustDecodeBounds=true;//只解析图片的宽高等信息,而不把图片加载到内存中去
		BitmapFactory.decodeFile(path, option);
		int outWidth = option.outWidth;
		int outHeight = option.outHeight;
		int width=imageSize.width;
		int height=imageSize.height;
		int inSimpleSize=1;
		if(outHeight>height||outWidth>height){
			int widthRadio=Math.round(outWidth*1.0f/width);
			int heightRadio=Math.round(outHeight*1.0f/height);
			inSimpleSize=Math.max(widthRadio, heightRadio);
		}
		option.inJustDecodeBounds=false;
		option.inSampleSize=inSimpleSize;
		
		return BitmapFactory.decodeFile(path, option);
	}
	/**
	 * 根据ImageView获取适当的压缩后的宽和高
	 * @param iv
	 * @return
	 */
	private ImageSize getImageSize(ImageView iv){
		LayoutParams lp = iv.getLayoutParams();
		//获取屏幕的矩阵
		DisplayMetrics dm = iv.getContext().getResources().getDisplayMetrics();
		int width=iv.getWidth();
		if(width<=0){
			width=lp.width;
		}
		if(width<=0){
			//使用反射获取ImageView的MaxWidth和MaxHeight兼容低版本
			width=getMaxValueFromInvoke(iv,"mMaxWidth");
		}
		if(width<=0){
			width=dm.widthPixels;
		}
		
		int height=iv.getHeight();
		if(height<=0){
			height=lp.width;
		}
		if(height<=0){
			height=getMaxValueFromInvoke(iv,"mMaxHeight");
		}
		if(height<=0){
			height=dm.heightPixels;
		}
		return new ImageSize(width, height);
	}
	/**
	 * 使用反射获取ImageView的MaxWidth和MaxHeight兼容低版本
	 * @param obj
	 * @param fieldName
	 * @return
	 */
	private int getMaxValueFromInvoke(Object obj,String fieldName){
		int value=0;
		try {
			Field field = ImageView.class.getDeclaredField(fieldName);
			field.setAccessible(true);
			int fieldValue = field.getInt(obj);
			if(fieldValue>0&&fieldValue


最后要是想在外界调用ImageLoader的时候指定Type属性和线程池最大维护线程个数的话,可以再类中增加一个getInstance的方法重载

/**
	 * 获取ImageLoader的单利方法的重载
	 * @return
	 */
	public static ImageLoader getInstance(int threadCount,Type type){
		//这里采用双重if判断,可以提高代码的执行效率
		//外层判断可能会有几个线程进到里面,然后线程同步加条件判断
		//保证单例唯一性
		if(instance==null){
			synchronized (ImageLoader.class) {
				if(instance==null){
					instance=new ImageLoader(threadCount,type);
				}
			}
		}
		return instance;
	}



项目源码下载地址:下载源码



你可能感兴趣的:(android)