android学习之路(六)---- 图片加载库的优化、封装

封装Image-Loader
一、背景
        universal-image-loader是一项伟大的开源项目,作者在其中运用到的软件工程解决办法让人印象深刻,在本篇文章的开篇,首先向universal-image-loader的作者致以敬意,详细地址:https://github.com/nostra13/Android-Universal-Image-Loader ,(源码详解可以参考:http://a.codekk.com/detail/Android/huxian99/Android%20Universal%20Image%20
Loader%20%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90
),相对于一个轻量级的app而言,universal-image-loader完全能够承担相关开发工作,但越到后来,对于体量相对较大的app而言,universal-image-loader的缺点逐渐显现出来(以下内容用u代表universal-image-loader):
        1.u的下载和保存在同一子线程进行,这就造成了下载到显示的过程有时间的浪费
        2.u的线程池高达三个,虽然每个线程池的管理方式不一样,且可自定义,但是如果一次性加载大量图片,(比如照片流),会消耗大量内存,本人在实验的过程当中,一次性加载60张图片,小米4上面还是能够加载出来,但是很卡,当加载图片的数量增加到70张的时候,不仅是app挂了,手机也挂了!如果用u来加载相册,会感受得比较明显
        3.u 封装的效果有限,在日常开发工作当中,显示的图片效果可能默认的效果,也可能是经过特殊裁剪和设计的效果,比如圆角矩形、圆形、圆形+环形、高斯模糊、LOMO效果等特效等等,所以需要进行扩展


二、结果
        进行相关改造和封装之后,基本解决了上面的问题,代码详见: https://github.com/pinguo-fandong/Fan-Image-Loader, 整个项目只有一个线程池,负责加载网络数据,而对于图片数据的缓存,用了一个Thread+Queue的方式,这样一来,整个项目的CPU消耗就只有一个线程池+一个子线程,性能提高不少,相较u而言,提高了图片的加载速度,减少了资源消耗
三、详细的解决办法
1. 准备工作
        首先看了u的所有源码,通过别人的分析和自己的理解,基本明白了整个流程,为了增加程序的可扩展性,采用builder模式进行封装。

2.修改缓存过程
     2.1 在接口DiskCache.java当中增加两个方法

/**
 * 从memorycache当中拿到bitmap,然后保存到sd卡上面
 *
 * @param cacheKey 图片对应的内存缓存的key和sd卡上面的缓存key
 * @return 是否保存成功
 * @throws IOException
 */
boolean save(String cacheKey) throws IOException;

/**
 * 通过生成的cache Key保存图片到缓存路径
 * @param cacheKey 缓存key(缓存的文件名称)
 * @param bitmap 图片
 * @return
 * @throws IOException
 */
boolean saveByCacheKey(String cacheKey, Bitmap bitmap) throws IOException;

     2.2 自定义本地缓存策略

/**
 * time: 15/11/17
 * description: 自定义的本地缓存机制
 *
 * @author fandong
 */
public class CustomDiskCache extends BaseDiskCache {
   
    private ConcurrentLinkedQueue<String> mQueue;
    //标识是否正在轮循
    private boolean mIsPoll;
    //标识是否销毁
    private boolean mIsDestroy;

    public CustomDiskCache(File cacheDir, File reserveCacheDir) {
        super(cacheDir, reserveCacheDir);
        this.mQueue = new ConcurrentLinkedQueue<String>();
    }


    @Override
    public boolean save(String cacheKey) throws IOException {
        if (!mQueue.contains(cacheKey)) {
            mQueue.add(cacheKey);
        }
        if (!mIsPoll) {
            mIsPoll = true;
            Thread thread = new Thread(getCacheTask());
            thread.start();
        }
        return true;
    }

    public Runnable getCacheTask() {
        return new Runnable() {
            @Override
            public void run() {
                try {
                    String cacheKey = mQueue.poll();
                    do {
                        //0.如果销毁就跳出线程
                        if (mIsDestroy) {
                            break;
                        }
                        boolean savedSuccessfully = false;
                        //1.从内存当中拿出缓存
                        Bitmap bitmap = FanImageLoader.getMemoryCache(cacheKey);
                        if (bitmap == null || bitmap.isRecycled()) {
                            continue;
                        }
                        //2.保存到sd卡上面
                        File imageFile = getFileByCacheKey(cacheKey);
                        FileOutputStream os = new FileOutputStream(imageFile);
                        try {
                            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
                        } finally {
                            IoUtils.closeSilently(os);
                            if (!savedSuccessfully) {
                                if (imageFile.exists()) {
                                    imageFile.delete();
                                }
                            }
                        }
                    } while ((cacheKey = mQueue.poll()) != null);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    mIsPoll = false;
                }
            }
        };
    }


    @Override
    public void close() {
        this.mIsDestroy = true;
        if (mQueue != null) {
            mQueue.clear();
        }
    }
}

     这就是将图片缓存到本地的核心部分了,由于从网络上面下载好图片之后,经过相应的图片处理(裁剪、加特效)等,会将bitmapkey-value的形式缓存在内存当中,当需要缓存到sd卡上面的时候,只需要从内存缓存当中拿到bitmap就可以了,那么怎样能够拿到内存当中的缓存bitmap呢?在FanImageLoader当中设计了这样的方法:

public static Bitmap getMemoryCache(String memoryKey) {
    return mImageLoader.getMemoryCache().get(memoryKey);
}

     2.3 本地缓存时机
     在u当中,加载图片是首先会从内存当中读取数据,如果没有缓存,会到sd卡上面去读取缓存文件,如果没有缓存文件,会到网络或者其他源获取数据,获取成功之后会首先放在内存当中,然后同步放入sd卡,然后显示在界面上面,整个加载本地缓存和缓存网络图片到sd卡上面,都在LoadAndDisplayImageTask.java里面,在tryLoadBitmap()方法当中,做如下修改:

private Bitmap tryLoadBitmap() throws TaskCancelledException {
    Bitmap bitmap = null;
    try {
        //2.从sd卡上面得到带宽高的缓存文件
        File imageFile = configuration.diskCache.getFileByCacheKey(memoryCacheKey);
        //3.如果没有带宽高的缓存文件,那么
        if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
            loadedFrom = LoadedFrom.DISC_CACHE;
            checkTaskNotActual();
            bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
        }
        if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
            loadedFrom = LoadedFrom.NETWORK;
            if (options.isCacheOnDisk()) {
                bitmap = tryCacheImageOnDisk();
            }
            if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                fireFailEvent(FailType.DECODING_ERROR, null);
            }
        }
    } catch (IllegalStateException e) {
        fireFailEvent(FailType.NETWORK_DENIED, null);
    } catch (TaskCancelledException e) {
        throw e;
    } catch (OutOfMemoryError e) {
        L.e(e);
        fireFailEvent(FailType.OUT_OF_MEMORY, e);
    } catch (Throwable e) {
        L.e(e);
        fireFailEvent(FailType.UNKNOWN, e);
    }
    return bitmap;
}

     从上面的过程可以看到,到sd卡上面读取缓存,如果没有就会去加载远程的图片资源,这个过程在tryCacheImageOnDisk()方法当中完成,下载的具体过程由 downloadImage()方法完成,做如下修改:

private Bitmap downloadImage() throws IOException {
    InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
    if (is == null) {
        L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
        return null;
    } else {
        try {
            Bitmap bitmap = null;
            //String url = uri;
            int width = targetSize.getWidth();
            int height = targetSize.getHeight();
            if (width > 0 || height > 0) {
                bitmap = BitmapUtils.createScaledBitmap(is, width, height);
            }
            if (bitmap == null) {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inPreferredConfig = Bitmap.Config.RGB_565;
                options.inSampleSize = 1;
                bitmap = BitmapFactory.decodeStream(is, null, options);
            }
            if (bitmap != null) {
                //1.存放在内存当中
                if (options.isCacheInMemory()) {
                    configuration.memoryCache.put(memoryCacheKey, bitmap);
                }
                //2.存放到sd卡上面
                if (uri.startsWith("content") || uri.startsWith("http")) {
                    configuration.diskCache.save(memoryCacheKey);
                }

            }
            return bitmap;
        } finally {
            IoUtils.closeSilently(is);
        }
    }
}

     需要说明的是,在封装的过程当中,存放到memory里面的key和存放到sd卡上面的key采用统一的生成方式,从上面的过程不难看出,存放数据到sd卡的过程是一个异步的过程,下载得到bitmap之后,会将memoryCacheKey传递给 CustomDiskCache,这样,CustomDiskCache就可以根据memoryCacheKey取出bitmap,然后进行存放。
     从上面的方法中不难看出,得到网络bitmap之后,程序对bitmap进行了裁剪,就是这句代码:

int width = targetSize.getWidth();
int height = targetSize.getHeight();
if (width > 0 || height > 0) {
    bitmap = BitmapUtils.createScaledBitmap(is, width, height);
}

     targetSize就是我们需要显示图片的ImageView或者ImageSwitcher,它的确定可以通过FanImageLoader.create("http://a.jpg").setShowSize(100,100)来确定,也可以通过给ImageView或者ImageSwitcher设置宽高,或者MaxWidth/MaxHeight实现。
     2.4 缓存key的生成
     缓存key默认是url+尺寸信息生成的md5码形成的,这样一来,同一个url会根据不同的size进行存储,大大加快了图片的加载速度,也是软件

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值