ImageLoader加载本地图片的实现————防OOM

这段时间,项目功能做完了,但是一直被OOM的问题困扰,在同样的代码在模拟机上可以运行,但在真机上却无法运行,报OOM,于是,最近开始接触关于OOM的相关问题,今天先总结一下关于ImageLoader的实现。
实现ImageLoader主要用到了下面几个知识点:
1.缓存LruCache。
2.线程池ExecutorService。
3.算法实现调度方式优化:LIFO(后进先出),FIFO(先进先出)。
4.任务队列LinkedList< Runnable >
5.后台轮询线程mPoolThread。
6.Handler的消息通讯。

知识点总结完毕后,说一下这个实现思路。
ImageLoader实现思路:
ImagLoader总体来说优化方式有几个点:
1.图片加载策略:使用LIFO而不是FIFO(一共有1000张图片,当你要查看第900张时肯定不能加载900张图片来显示第900张,可以只加载900张开始的图片,前面的图片不用加载)
2.图片压缩策略:对一个需要加载的图片,对ImageView的组件大小进行判断,和图片的实际大小进行比对,当图片过大时,利用Options进行压缩,压缩完成后再使用。
3.使用LruCache:设定一个固定大小的缓存空间(一般为dalvik虚拟机为应用分配的内存的1/8),当新的图片加载完成后先放入缓存,以后每次需要加载图片前,先到缓存中查找是否存在该图片,如果有,则直接从缓存中去,如果没有,再加载,这样避免了才加载完成不久的图片多次加载。
4.使用线程池,因为Android不允许在UI线程中执行耗时操作,所以加载图片时需要在新的线程中执行,但是,每加载一个图片就需要执行一个线程,那么大量图片加载的时候会占用大量内存,所以使用线程池,将固定同一时间只允许一定数量的线程在后台运行(自己设定,数量不要太大,3个左右),所有线程先加入线程池中。
用户传入url(图片地址)-> Bitmap
图片查找思路:
1.url->LruCache查找
->找到则返回Bitmap(这样避免同一张图片多次加载)
->找不到 ->url -> Task -> TaskQueue且发送一个通知给后台轮询线程
2.图片加载思路:
Task -> Run() 根据url 加载图片
1.获取图片显示大小
2.使用Options对图片进行压缩(防止图片过大导致OOM)
3.加载图片且放入LruCache(避免同一张图片多次加载)
后台轮询线程:
TaskQueue -> Task -> 线程池去执行(Handler+Looper+Message)

代码实现:
声明变量:

/**
*使用单例模型
*/
private static ImageLoader mInstance;
    /**
     * 图片缓存的核心对象
     */
    private LruCache<String, Bitmap> mLruCache;
    /**
     * 线程池
     */
    private ExecutorService mThreadPool;
    private static final int DEAFULT_THREAD_COUNT = 1;
    /**
     * 队列调度方式
     */
    private Type mType = Type.LIFO;
    /**
     * 任务队列
     */
    private LinkedList<Runnable> mTaskQueue;
    /**
     * 后台轮询线程
     */
    private Thread mPoolThread;
    private Handler mPoolThreadHandler;
    /**
     * UI线程中的Handler
     */
    private Handler mUIHandler;
    /**
    *信号量,用于多线程之间的资源同步
    */
    private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);

    private Semaphore mSemaphoreThreadPool;
    /**
    *枚举类型,用于加载策略
    */
    public enum Type {
        FIFO, LIFO;
    }

构造方法:

private ImageLoader(int threadCount, Type type) {
        init(threadCount, type);//传入默认线程池的线程个数,和加载策略
    }

    /*
     * 初始化操作
     */
    private void init(int threadCount, Type type) {
        // 后台轮询线程
        mPoolThread = new Thread() {
            @Override
            public void run() {
                Looper.prepare();//给线程创建一个消息循环
                mPoolThreadHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        // 线程池取出一个任务去执行
                        mThreadPool.execute(getTask());
                        try {
                            mSemaphoreThreadPool.acquire();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }

                };
                //释放一个信号量
                mSemaphorePoolThreadHandler.release();
                Looper.loop();//使消息循环起作用,Looper.loop()内部会结束整个子线程的执行, 所以Looper.loop()之后的语句是不会运行的
            }
        };
        /*mPoolThread = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                Looper.prepare();
                mPoolThreadHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        // 线程池取出一个任务去执行
                        mThreadPool.execute(getTask());
                        try {
                            mSemaphoreThreadPool.acquire();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }

                };
                //释放一个信号量
                mSemaphorePoolThreadHandler.release();
                Looper.loop();
            }
        });*/
        mPoolThread.start();

        // 获取我们应用的最大可用内存
        int MaxMemory = (int) Runtime.getRuntime().maxMemory();
        //设置缓存大小为最大可用内存的1/8
        int cacheMemory = MaxMemory / 8;
        mLruCache = new LruCache<String, Bitmap>(cacheMemory) {
            @Override
            protected int sizeOf(String key, Bitmap value) {// 重写此方法来衡量每张图片的大小
                return value.getRowBytes() * value.getHeight();//getRowBytes()获取Bitmap每一行所占用的内存字节数。
            }
        };

        //创建线程池
        mThreadPool = Executors.newFixedThreadPool(threadCount);//创建一个固定大小的线程池,线程池执行数量不能超过threadcount
        mTaskQueue = new LinkedList<Runnable>();
        mType = type;

        mSemaphoreThreadPool = new Semaphore(threadCount);//创建一个信号量为threadcount的许可集
    }
/**
     * 从任务队列取出一个方法
     * @return
     */
    private Runnable getTask() {
        if(mType == Type.FIFO){//先进先出,则从队列头开始取
            return mTaskQueue.removeFirst();
        }else if(mType == Type.LIFO)
        {//后进先出,则从队尾开始取
            return mTaskQueue.removeLast();
        }
        return null;
    }
public static ImageLoader getInstance(int threadCount,Type type) {
        // 懒加载,单例模型,一次初始化后,不需要再初始化
        if (mInstance == null) {
            // 同步资源锁,防止不同线程多次调用初始化ImageLoader类,占用内存,提高效率
            synchronized (ImageLoader.class) {

                if (mInstance == null) {
                    mInstance = new ImageLoader(threadCount, type);
                }
            }
        }
        return mInstance;
    }
/**
     *根据path为imageView设置图片
     * @param path
     * @param imageView
     */
    public void loadImage(final String path,final ImageView imageView){
    //为每个ImageView设置Tag防止图片加载位置错误
        imageView.setTag(path);
        if(mUIHandler == null){
            mUIHandler = new Handler(){
                public void handleMessage(Message msg) {
                    //获取得到图片,为imageview回调设置图片
                    ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
                    Bitmap bm = holder.bitmap;
                    ImageView imageview = holder.imageView;
                    String path = holder.path;
                    //将path与gettag存储路径进行比较
                    if(imageview.getTag().toString().equals(path)){
                        imageview.setImageBitmap(bm);
                    }
                };
            };
        }
        //根据path在缓存中获取Bitmap
        Bitmap bm = getBitmapFromLruCache(path);
        if(bm != null){
        //缓存中存在,在从使用缓存中的图片
            refreshBitmap(path, imageView, bm);
        }else{
        //缓存中不存在则加载图片
            addTask(new Runnable() {

                @Override
                public void run() {
                    //加载图片
                    //图片的压缩
                    //1.获得图片需要显示的大小
                    ImageSize imageSize = getImageViewSize(imageView);
                    //2.压缩图片
                    Bitmap bm = decodeSampledBitmapFromPath(path,imageSize.width,imageSize.height);
                    //3.把图片加入到缓存
                    addBitmapToLruCache(path,bm);

                    refreshBitmap(path, imageView, bm);
                    //释放一个信号量,使线程池可以进行再取一个任务执行
                    mSemaphoreThreadPool.release();
                }

            });
        }

    }
    private void refreshBitmap(final String path,
            final ImageView imageView, Bitmap bm) {
        Message message =Message.obtain();//从整个Messge池中返回一个新的Message实例,在许多情况下使用它,因为它能避免分配新的对象,避免内存开销
        ImgBeanHolder holder = new ImgBeanHolder();
        holder.bitmap = bm;
        holder.path = path;
        holder.imageView = imageView;
        message.obj = holder;
        mUIHandler.sendMessage(message);
    }
    /**
     * 将图片加入缓存
     * @param path
     * @param bm
     */
    protected void addBitmapToLruCache(String path, Bitmap bm) {
        // TODO Auto-generated method stub
        if(getBitmapFromLruCache(path) == null){
            if(bm!=null){
                mLruCache.put(path, bm);
            }
        }
    }

    /**
     * 根据图片需要显示的宽和高进行压缩
     * @param path
     * @param width
     * @param height
     * @return
     */
    protected Bitmap decodeSampledBitmapFromPath(String path, int width,
            int height) {
        //获取图片的宽和高并不把图片加载到内存中
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path,options);//options中保存图片的真实宽高

        options.inSampleSize = caculateInSampleSize(options,width,height);//处理得到压缩比例

        //使用获取到的InSampleSize再次解析图片
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(path,options);
        return bitmap;
    }
/**
 * 根据需求的宽和高以及图片实际的宽和高计算SampleSize
 * @param options
 * @param width
 * @param height
 * @return
 */
    private int caculateInSampleSize(Options options, int reqwidth, int reqheight) {
        int width = options.outWidth;
        int height = options.outHeight;
        int inSampleSize = 1;
        if(width >reqwidth || height > reqheight){
            int widthRadio = Math.round(width*1.0f/reqwidth);
            int heightRadio = Math.round(height*1.0f/reqheight);
            //比例可以根据自己的需要调整
            inSampleSize = Math.max(widthRadio, heightRadio);
        }

        return inSampleSize;
    }

    /**
     * 根据imageView获取适当的压缩的宽和高
     * @param imageView
     * @return
     */
    protected ImageSize getImageViewSize(ImageView imageView) {
         ImageSize imageSize = new ImageSize();
         DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();//将当前窗口的一些信息放在DisplayMetrics类中

         LayoutParams lp = imageView.getLayoutParams();
         int width = imageView.getWidth();//获取imageView的实际宽度
         if(width <= 0){
             width = lp.width;//获取imageview在layout中声明的宽度
         }
         if(width <= 0){
             //width = imageView.getMaxWidth();//检查最大值
             width = getImageViewFieldValue(imageView,"mMaxWidth");
         }
         if(width <= 0){
             width = displayMetrics.widthPixels;//屏幕宽度
         }


         int height = imageView.getHeight();//获取imageView的实际宽度
         if(height <= 0){
             height = lp.height;//获取imageview在layout中声明的宽度
         }
         if(height <= 0){
            // height = imageView.getMaxHeight();//检查最大值
             height = getImageViewFieldValue(imageView,"mMaxHeight");
         }
         if(height <= 0){
             height = displayMetrics.heightPixels;
         }

         imageSize.width = width;
         imageSize.height = height;

        return imageSize;
    }
    /**
     * 通过反射获取ImageView  的某个属性值
     * @param object
     * @param fieldName
     * @return
     */
    private static int getImageViewFieldValue(Object object,String fieldName){
        int value = 0;
        try {
        Field field = ImageView.class.getDeclaredField(fieldName);
        field.setAccessible(true);

        int fieldValue = field.getInt(object);
        if(fieldValue>0 && fieldValue<Integer.MAX_VALUE){
            value = fieldValue;
        }
        } catch (Exception e) {
        }
        return value;
    }
/**
 * 同步锁,只能由一个调用,防止多个同时调用,一次加入过多Runnable
 * @param runnable
 */
    private synchronized void addTask(Runnable runnable) {
        mTaskQueue.add(runnable);
        //if(mPoolThreadHandler == null) wait();
        try {
            if(mPoolThreadHandler == null)
            mSemaphorePoolThreadHandler.acquire();/信号量加1
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        mPoolThreadHandler.sendEmptyMessage(0x110);
    }

    /**
     * 根据path在缓存中获取Bitmap
     * @param key
     * @return
     */
    private Bitmap getBitmapFromLruCache(String key) {
        return mLruCache.get(key);
    }
    private class ImageSize {
        int width;
        int height;
    }
    private class ImgBeanHolder{
        Bitmap bitmap;
         ImageView imageView;
         String path;
    }

实现ImageLoader类的实现后,加载本地图片,只需一句话:
ImageLoader.getInstance(3, Type.LIFO).loadImage(
url, ImageView);
还是很方便的。

这次ImageLoader的实现让我感受到OOM的重要性,内存的管理优化真的很重要,以后要注重这方面的问题的空白。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值