自定义图片加载器

自定义图片加载器


前言

图片加载器是一个非常常用的功能模块,但是一般我们不会去从零开始自己写一个,因为有Glide、Picasso、Fresco等一些优秀的开源库,或者公司自己维护了一套。再者完整实现这一整套功能是很耗费精力的。而这篇文章仅仅是为了学习程序的设计模式和LruCache、Bitmap优化显示等,去实现这样的一个库。

先说下面向对象六大原则及在此项目里的体现:

1. 单一原则

简单来说,一个类中,应该是一组相关性很高的函数、数据的封装。具体根据类的职责。

2. 开闭原则

提取共同的函数为一个接口,通过设置接口对象或者实现该接口的对象,实现不同的缓存策略。

3. 里氏替换原则

在这里体现的就是只要实现IImageCache接口的都可以设置到缓存去。

4. 依赖倒置原则

模块间依赖通过抽象发生,实现类不直接发生依赖关系,其依赖是通过抽象类和接口。通过这样减少耦合。

5. 接口隔离原则

fileOutputStream等其他流的关闭,这里只要是实现Closeable接口的都可以关闭close();只要知道类实现了closeable,就可关闭,其他的一概不关心,这就是接口隔离。

6. 迪米特原则

ImageCache里面使用了DiskLruCache,但是用户不需要知道实现细节,只需要和ImageCache打交道。即使里面的DiskLruCache替换为其他的缓存实现,用户也不会感知到。


下面开始分析

ImageLoader:负责下载图片和加载的类.

ImageCache:含有LruCache和DiskLruCache.

ImageResizer:获取特定采样的bitmap.

IImageCacahe:抽象ImageCache的接口,之前说的六大原则,很重要的一点是强调抽象.

ImageCache里面使用了LruCache和DiskLruCache:

 lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };       
        try {
            diskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }

这里缓存大小cacheSize设置为当前进程可用内存的0.25倍.sizeOf完成对Bitmap对象大小的计算。
lruCache缓存和读取:
lruCache.put(key, bitmap), Bitmap bitmap = lruCache.get(key);
DiskLruCache缓存和读取:

 public void addToDiskCache(Bitmap bitmap, String key) throws IOException {
        DiskLruCache.Editor editor = diskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (addToDisk(bitmap, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            diskLruCache.flush();
            outputStream.close();
        }
    }
try {
                DiskLruCache.Snapshot snapShot = diskLruCache.get(key);
                if (snapShot != null) {
                    FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
                    FileDescriptor fileDescriptor = fileInputStream.getFD();
                    bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, width, height);
                    if (bitmap != null)
                        addToCache(bitmap, url);
                    return bitmap;
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.e(TAG, "getDiskLruCache error");
            }

BitmapFactory.decodeStream(in),不能decode两次。所以用ImageResizer.decodeSampledBitmapFromFileDescriptor根据ImageView大小获取对应采样率的Bitmap(因为ImageView小,而图片的分辨率大时,加载原图是很浪费内存的)。主要是计算options.inSampleSize:

public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

ImageLoader里面先从ImageCache中读取,若是没有,者从网络下载,但是第一次下载获取的Bitmap不能直接设置到ImageView中去。

private IImageCacahe cache;
private Bitmap downloadBitmapFromUrl(String urlString, int width, int height) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
            if (cache instanceof ImageCache)
                ((ImageCache) cache).addToDiskCache(bitmap, Utils.keyFormUrl(urlString));
            bitmap.recycle();
            bitmap = cache.getFromCache(urlString, width, height);// TODO: 2017/4/4  这里因为第一次下载的原图需要重新采样获取需求大小的bitmap,而BitmapFactory.decodeStream(in),不能decode两次。
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmap: " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            Utils.close(in);
        }
        return bitmap;
    }

根据imageview的大小,通过BitmapFactory.Options options,,options.inSampleSize,计算需要的inSampleSize压缩得到bitmap再加载到imageview上去。
若未采样优化,加载一屏6张图片原图,总20张,上下滑动时,内存占用情况如下:
这里写图片描述
优化后内存占用情况如下 :这里只是指定了下需要的bitmap显示的大小(单位为像素,不同的设备上相同的dp显示的像素不一致,所以大小也会不同)。根据需要的大小及其FileDescriptor,从其DiskLruCache获取bitmap,再显示到ImageVIew中去。
这里写图片描述
对比内存占用区别相当大,但是显示的效果一致并没有打折扣。

还有需要解决的一个问题是ImageView的复用,例如在Listview中加载图片,如果需要加载的图片用户已经划过去了,那么应该忽略这张图片,具体的就是在 target.setTag(TAG_KEY_URI, url),线程池的任务中 handler.obtainMessage(SUCCESS_COMPLETE, new LoaderResult(target, url, bitmap)).sendToTarget();然后在handler的handleMessage中
imageView.getTag(TAG_KEY_URI);对比url是否一致,不一致则忽略该图片。

最后顺便接入了leakcanary,发现内存泄漏:华为mate8上Android6.0的HwPhoneWindow的mContext持有MainActivity对象,导致泄漏;红米note2上Android5.0:未发现有内存泄漏。这里是华为的系统问题,不知有没有大神能指点这该如何解决,感激不尽。
项目源码地址:https://github.com/Ulez/UImageLoader
参考资料:
1.Android开发艺术探索
2.Android源码设计模式解析与实战

发布了29 篇原创文章 · 获赞 4 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/s122ktyt/article/details/69357392