查看原文
其他

Bitmap的加载和Cache

胡飞洋 胡飞洋 2022-07-18

Bitmap,表示位图,由像素点构成。Bitmap的承载容器是jpg、png等格式的文件,是对bitmap的压缩。当jpg、png等文件需要展示在手机上的控件时,就会解析成Bitmap并绘制到view上。通常处理图片时要避免过多的内存使用,毕竟移动设备的内存有限。

那么加载一张图片需要占用多大内存呢?考虑到效率加载图片时缓存策略是怎样的呢?



01

Bitmap的加载


1.1 Bitmap的内存占用

原始计算公式如下:

Bitmap的内存 = 分辨率 * 像素点的大小

  • 图片分辨率,可能不是原始图片的分辨率。例如  图片放在Res中不同dpi的文件夹中,分辨率是原始分辨率转换后的。比如放hdpi与放xhdpi,转换后的分辨率是不同的。转换后的分辨率=原始分辨率*(设备的 dpi / 目录对应的 dpi)。其他情况,如放在磁盘、文件、流等均按原分辨率处理。另外,这个逻辑是原生系统BitmapFactory的逻辑,如果是直接使用图片库,内部逻辑可能不会转换分辨率,如glide的不转换Res中图片的分辨率。

  • 像素点的大小,就是ARGB8888(4B)、RGB565(2B)这几个。


1.2 Bitmap的高效加载


Bitmap的加载,可过系统提供的BitmapFactory四个方法:decodeFile、decodeResource、decodeStream、decodeByteArray,对应处理从 文件、资源、流、字节数组 来加载Bitmap。


如何优化加载呢?由公式可见想要减少图片加载成Bitmap时占用的内存,两个方法:

  • 降低像素点的大小:如可以把图片格式ARGB8888 换成RGB565,内存占用就会减少一半,但会降低。但导致不支持透明度,降低图片质量。开源库一般也支持更换格式。

  • 降低分辨率:通常图片的分辨率远大于控件view的分辨率,加载后view无法显示原始的分辨率,所以降低分辨率也不会影响图片的展示效果。


针对第二点,降低分辨率,BitmapFactory.Options也提供了对应的方法,步骤如下:

  1. 把BitmapFactory.Options.inJustDecodeBounds设为true,并加载图片。(只加载原始宽高信息,轻量级操作)

  2. 获取原始宽高信息:options.outWidth、options.outHeight

  3. 设置采样率options.inSampleSize。采样率根据 原始宽高信息 和 view的大小计算。

  4. 把BitmapFactory.Options.inJustDecodeBounds设为false,并加载图片。

代码如下:


1    private void initView() {
2        //R.mipmap.blue放在Res的xxh(480dpi)中,测试手机dpi也是480
3
4        //1、inJustDecodeBounds设为true,并加载图片
5        BitmapFactory.Options options = new BitmapFactory.Options();
6        options.inJustDecodeBounds = true;
7        BitmapFactory.decodeResource(getResources(), R.mipmap.blue, options);
8
9        //2、获取原始宽高信息
10        int outWidth = options.outWidth;
11        int outHeight = options.outHeight;
12
13        Log.i(TAG, "initView: outWidth="+outWidth+",outHeight="+outHeight);
14
15        //3、原始宽高信息 和 view的大小 计算并设置采样率
16        ViewGroup.LayoutParams layoutParams = ivBitamp.getLayoutParams();
17        int inSampleSize = getInSampleSize(layoutParams.width, layoutParams.height, outWidth, outHeight);
18        options.inSampleSize = inSampleSize;
19
20        Log.i(TAG, "initView: inSampleSize="+options.inSampleSize);
21
22        //4、inJustDecodeBounds设为false,并加载图片
23        options.inJustDecodeBounds = false;
24        Bitmap bitmap= BitmapFactory.decodeResource(getResources(), R.mipmap.blue, options);
25
26        Log.i(TAG, "initView: size="+bitmap.getByteCount());
27
28        int density = bitmap.getDensity();
29        Log.i(TAG, "initView: density="+density);
30        Log.i(TAG, "initView: original size="+337*222*4);
31        Log.i(TAG, "initView: calculated size="+ (337/inSampleSize) *(222/inSampleSize)* density/480 *4);
32
33        //绘制到view
34        ivBitamp.setImageBitmap(bitmap);
35    }
36
37    /**
38     * 计算采样率
39     * @param width view的宽
40     * @param height view的高
41     * @param outWidth 图片原始的宽
42     * @param outHeight 图片原始的高
43     * @return
44     */

45    private int getInSampleSize(int width, int height, int outWidth, int outHeight) {
46        int inSampleSize = 1;
47        if (outWidth>width || outHeight>height){
48            int halfWidth = outWidth / 2;
49            int halfHeight = outHeight / 2;
50            //保证采样后的宽高都不小于目标快高,否则会拉伸而模糊
51            while (halfWidth/inSampleSize >=width
52                    && halfHeight/inSampleSize>=height){
53                //采样率一般取2的指数
54                inSampleSize *=2;
55            }
56        }
57
58        return inSampleSize;
59    }
60}



02


Android中的缓存策略


缓存策略在Android中应用广泛。使用缓存可以节省流量、提高效率


加载图片时,一般会从网络加载,然后缓存在存储设备上,这样下次就不用请求网络了。并且通常也会缓存一份到内存中,这样下次可以直接取内存中的缓存,要比从存储设备中取快很多。所以一般是先从内存中取,内存没有就取存储设备,也没有才会请求网络,这就是所谓的“三级缓存”。此策略同样适用其他文件类型。


缓存策略中的操作有 添加缓存、获取缓存、删除缓存。添加和获取比较好理解,删除缓存是啥意思?因为缓存大小是有限制的,像移动设备的 内存 和 设备存储都是有限的,不能无限制的添加,只能限定一个最大缓存,到达最大时就会删除一部分缓存。但是删除哪一部分缓存呢?删除 缓存创建时间最老的吗,如果它经常用到呢,好像不太完美,当然这也是一种缓存算法。

目前经典的缓存算法是LRU(Least Recently Used),最近最少使用。具体就是 当缓存满时,会先删除那些 近期 最少使用 的缓存。使用LRU算法的缓存有两种,LruCache和DiskLruCache,LruCache是使用内存缓存,DiskLruCache是实现磁盘缓存。


2.1 LruCache


LruCache是泛型类,使用方法如下:
提供最大缓存容量,创建LruCache实例,并重写其sizeOf方法来计算缓存对象的大小。最大容量和缓存对象大小单位要一致。


1    private void testLruCache() {
2        //当前进程的最大内存,单位M
3        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
4        //取进程内存的1/8
5        int cacheMaxSize = (int) (maxMemory/8);
6        //创建Bitmap实例
7        mBitmapLruCache = new LruCache<String, Bitmap>(cacheMaxSize){
8            @Override
9            protected int sizeOf(String key, Bitmap value) {
10                //缓存对象bitmap的大小,单位M
11                return value.getByteCount()/1024/1024;
12            }
13
14            @Override
15            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
16                //移除旧缓存时会调用,可以在这里进行像资源回收的工作。
17                //evicted为true,表示此处移除是因为快满了要腾出空间
18            }
19        };
20
21        //添加缓存
22        mBitmapLruCache.put("1",mBitmap);
23
24        //获取缓存
25        Bitmap bitmap = mBitmapLruCache.get("1");
26        ivBitamp.setImageBitmap(bitmap);
27
28        //删除缓存,一般不会用,因为快满时会自动删近期最少使用的缓存,就是它的核心功能
29        mBitmapLruCache.remove("1");
30    }


可见使用很简单,那么LruCache是怎么完成 删除“近期最少使用” 的呢?看下LruCache的代码:


1public class LruCache<K, V> {
2    //此map以强引用的方式存储缓存对象
3    private final LinkedHashMap<K, V> map;
4    //当前缓存的大小(带单位的)
5    private int size;
6    //缓存最大容量(带单位的)
7    private int maxSize;
8    ...
9    public LruCache(int maxSize) {
10        if (maxSize <= 0) {
11            throw new IllegalArgumentException("maxSize <= 0");
12        }
13        this.maxSize = maxSize;
14        //LinkedHashMap是按照 访问顺序 排序的,所以get、put操作都会把要存的k-v放在队尾
15        this.map = new LinkedHashMap<K, V>(00.75ftrue);
16    }
17
18    /**
19     * 获取缓存,同时会把此k-v放在链表的尾部
20     */

21    public final V get(K key) {
22        if (key == null) {
23            throw new NullPointerException("key == null");
24        }
25
26        V mapValue;
27        //get是线程安全的操作
28        synchronized (this) {
29            //LinkedHashMap的get方法中调afterNodeAccess,会移到链表尾部
30            mapValue = map.get(key);
31            if (mapValue != null) {
32                hitCount++;
33                return mapValue;
34            }
35            missCount++;
36        }
37        ...
38    }
39
40    /**
41     * 缓存key-value,value会存在 队尾
42     * @return 之前也是这个key存的value
43     */

44    public final V put(K key, V value) {
45        if (key == null || value == null) {
46            //不允许 null key、null value
47            throw new NullPointerException("key == null || value == null");
48        }
49
50        V previous;
51        //可见put操作是线程安全的
52        synchronized (this) {
53            putCount++;
54            size += safeSizeOf(key, value);
55            //强引用存入map(不会被动地被系统回收),其因为是LinkedHashMap,会放在队尾
56            previous = map.put(key, value);
57            if (previous != null) {
58                //如果前面已这个key,那么替换后调整下当前缓存大小
59                size -= safeSizeOf(key, previous);
60            }
61        }
62
63        if (previous != null) {
64            entryRemoved(false, key, previous, value);
65        }
66        //重新调整大小
67        trimToSize(maxSize);
68        return previous;
69    }
70
71    /**
72     * 比较 当前已缓存的大小 和最大容量,决定 是否删除
73     */

74    private void trimToSize(int maxSize) {
75        while (true) {
76            K key;
77            V value;
78            synchronized (this) {
79                if (size < 0 || (map.isEmpty() && size != 0)) {
80                    throw new IllegalStateException(getClass().getName()
81                            + ".sizeOf() is reporting inconsistent results!");
82                }
83
84                if (size <= maxSize) {
85                    //大小还没超过最大值
86                    break;
87                }
88
89                //已经达到最大容量
90
91                //因为是访问顺序,所以遍历的最后一个就是最近没有访问的,那么就可以删掉它了!
92                Map.Entry<K, V> toEvict = null;
93                for (Map.Entry<K, V> entry : map.entrySet()) {
94                    toEvict = entry;
95                }
96                // END LAYOUTLIB CHANGE
97
98                if (toEvict == null) {
99                    break;
100                }
101
102                key = toEvict.getKey();
103                value = toEvict.getValue();
104                map.remove(key);
105                size -= safeSizeOf(key, value);
106                evictionCount++;
107            }
108            //因为是为了腾出空间,所以这个回调第一个参数是true
109            entryRemoved(true, key, value, null);
110        }
111    }
112
113    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
114
115    ...
116}


由以上代码及注释,可见LruCache的算法实现是依靠 设置了访问顺序的LinkedHashMap。因为是访问顺序模式,get、put操作都会调整k-v到链表尾部。在缓存将满时,遍历LinkedHashMap,因为是访问顺序模式,所以遍历的最后一个就是最近没有使用的,然后删除即可。


2.2 DiskLruCache


DiskLruCache是实现磁盘缓存,所以需要设备存储的读写权限;一般是从网络请求图片后缓存到磁盘中,所以还需要网络权限。

1    <uses-permission android:name="android.permission.INTERNET" />
2    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
3    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>


DiskLruCache,不是官方提供,所以需要引入依赖:

1implementation 'com.jakewharton:disklrucache:2.0.2'
  • DiskLruCache的创建,不是通过new,而是open方法,需要传入缓存目录、最大缓存容量。

  • 缓存的添加,是通过Editor,缓存对象的编辑器。传入图片url的key 调用DiskLruCache的edit方法获取Editor(如果缓存正在被编辑就会返回null),可以从Editor得到文件输出流,这样就可以写入到文件系统了。

  • 缓存的获取,传入图片url的key 调用DiskLruCache的get方法 得到SnapShot,可从SnapShoty获取文件输入流,这样就用BitmapFactory得到bitmap了。

  • 缓存的删除,DiskLruCache的remove方法可以删除key对应的缓存。

    通过查看源码,发现LinkedHashMap内部也是维护了访问顺序的LinkedHashMap,原理上和LruCache是一致的。只是使用上有点点复杂,毕竟涉及文件的读写。


具体使用及注意点如下代码:

1    private void testDiskLruCache(String urlString) {
2        long maxSize = 50*1024*1024;
3
4        try {
5            //一、创建DiskLruCache
6            //第一个参数是要存放的目录,这里选择外部缓存目录(若app卸载此目录也会删除);
7            //第二个是版本一般设1;第三个是缓存节点的value数量一般也是1;
8            //第四个是最大缓存容量这里取50M
9            mDiskLruCache = DiskLruCache.open(getExternalCacheDir(), 11, maxSize);
10
11            //二、缓存的添加:1、通过Editor,把图片的url转成key,通过edit方法得到editor,然后获取输出流,就可以写到文件系统了。
12            DiskLruCache.Editor editor = mDiskLruCache.edit(hashKeyFormUrl(urlString));
13            if (editor != null) {
14                //参数index取0(因为上面的valueCount取的1)
15                OutputStream outputStream = editor.newOutputStream(0);
16                boolean downSuccess = downloadPictureToStream(urlString, outputStream);
17                if (downSuccess) {
18                    //2、编辑提交,释放编辑器
19                    editor.commit();
20                }else {
21                    editor.abort();
22                }
23                //3、写到文件系统,会检查当前缓存大小,然后写到文件
24                mDiskLruCache.flush();
25            }
26        } catch (IOException e) {
27            e.printStackTrace();
28        }
29
30        //三、缓存的获取
31        try {
32            String key = hashKeyFormUrl(urlString);
33            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
34            FileInputStream inputStream = (FileInputStream)snapshot.getInputStream(0);
35//            Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
36//            mIvBitamp.setImageBitmap(bitmap);
37
38            //注意,一般需要采样加载,但文件输入流是有序的文件流,采样时两次decodeStream影响文件流的文职属性,导致第二次decode是获取是null
39            //为解决此问题,可用文件描述符
40            FileDescriptor fd = inputStream.getFD();
41
42            //采样加载(就是前面讲的bitmap的高效加载)
43            BitmapFactory.Options options = new BitmapFactory.Options();
44            options.inJustDecodeBounds=true;
45            BitmapFactory.decodeFileDescriptor(fd,null,options);
46            ViewGroup.LayoutParams layoutParams = mIvBitamp.getLayoutParams();
47            options.inSampleSize = getInSampleSize(layoutParams.width, layoutParams.height, options.outWidth, options.outHeight);
48            options.inJustDecodeBounds = false;
49            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
50
51            //存入内容缓存,绘制到view。(下次先从内存缓存获取,没有就从磁盘缓存获取,在没有就请求网络--"三级缓存")
52            mBitmapLruCache.put(key,bitmap);
53
54            runOnUiThread(new Runnable() {
55                @Override
56                public void run() {
57                    mIvBitamp.setImageBitmap(mBitmapLruCache.get(key));
58                }
59            });
60
61        } catch (IOException e) {
62            e.printStackTrace();
63        }
64    }
65
66    /**
67     * 下载图片到文件输入流
68     */

69    private boolean downloadPictureToStream(String urlString, OutputStream outputStream) {
70        URL url = null;
71        HttpURLConnection urlConnection = null;
72        BufferedInputStream in = null;
73        BufferedOutputStream out = null;
74        try {
75            url = new URL(urlString);
76            urlConnection = (HttpURLConnection) url.openConnection();
77            in = new BufferedInputStream(urlConnection.getInputStream());
78            out = new BufferedOutputStream(outputStream);
79
80            int b;
81            while ((b=in.read()) != -1) {
82                //写入文件输入流
83                out.write(b);
84            }
85            return true;
86        } catch (IOException e) {
87            e.printStackTrace();
88        }finally {
89            if (urlConnection != null) {
90                urlConnection.disconnect();
91            }
92            try {
93                if (in != null) {in.close();}
94                if (out != null) {out.close();}
95            } catch (IOException e) {
96                e.printStackTrace();
97            }
98        }
99        return false;
100    }
101
102    /**
103     * 图片的url转成key,使用MD5
104     */

105    private String hashKeyFormUrl(String url) {
106        try {
107            MessageDigest digest = MessageDigest.getInstance("MD5");
108            return byteToHexString(digest.digest(url.getBytes()));
109        } catch (NoSuchAlgorithmException e) {
110            e.printStackTrace();
111        }
112        return null;
113    }
114
115    private String byteToHexString(byte[] bytes) {
116        StringBuffer stringBuffer = new StringBuffer();
117        for (int i = 0; i < bytes.length; i++) {
118            String hex = Integer.toHexString(0XFF & bytes[i]);
119            if (hex.length()==1) {
120                stringBuffer.append(0);
121            }
122            stringBuffer.append(hex);
123        }
124        return stringBuffer.toString();
125    }



02


ImageLoader


前面说的 Bitmap的高效加载、LruCache、DiskLruCache,是一个图片加载框架必备的功能点。下面就来封装一个ImageLoader。首先罗列 实现的要点

  • 图片压缩,就是采样加载

  • 内存缓存,LruCache

  • 磁盘缓存,DiskLruCache

  • 网络获取,请求网络url

  • 同步加载,外部子线程同步执行

  • 异步加载,ImageLoader内部线程异步执行


说明,

  1. 三级缓存“的逻辑:加载时 先从内存缓存获取,有就返回bitmap绘制图片到view,若没有就从磁盘缓存获取;磁盘缓存有就返回bitmap并缓存到内存缓存,没有就请求网络;网络请求回来,就缓存到磁盘缓存,然后从磁盘缓存获取返回。

  2. 同步加载,是在外部的子线程中执行,同步加载方法内部没有开线程,所以加载过程是耗时的 会阻塞 外部的子线程,加载完成后 需要自行切到主线程绘制到view。

  3. 异步加载,外部可在任意线程执行,因为内部实现是在子线程(线程池)加载,并且内部会通过Handler切到主线程,只需要传入view,内部就可直接绘制Bitmap到view。


详细如下

1public class ImageLoader {
2
3    private static final String TAG = "ImageLoader";
4
5    private static final long KEEP_ALIVE_TIME = 10L;
6
7    private static final int CPU_COUNT =  Runtime.getRuntime().availableProcessors();
8
9    private static final int CORE_THREAD_SIZE = CPU_COUNT + 1;
10
11    private static final int THREAD_SIZE = CPU_COUNT * 2 + 1;
12
13    private static final int VIEW_TAG_URL = R.id.view_tag_url;
14
15    private static final Object object = new Object();
16
17
18    private ThreadPoolExecutor mExecutor;
19
20    private Handler mMainHandler;
21
22
23    private Context mApplicationContext;
24
25    private static volatile ImageLoader mImageLoader;
26
27    private LruCache<String, Bitmap> mLruCache;
28
29    private DiskLruCache mDiskLruCache;
30
31    /**
32     * 磁盘缓存最大容量,50M
33     */

34    private static final long DISK_LRU_CACHE_MAX_SIZE = 50 * 1024 * 1024;
35
36    /**
37     * 当前进程的最大内存,取进程内存的1/8
38     */

39    private static final long MEMORY_CACHE_MAX_SIZE = Runtime.getRuntime().maxMemory() / 8;
40
41
42    public ImageLoader(Context context) {
43        if (context == null) {
44            throw new RuntimeException("context can not be null !");
45        }
46        mApplicationContext = context.getApplicationContext();
47
48        initLruCache();
49        initDiskLruCache();
50        initAsyncLoad();
51    }
52
53    public static ImageLoader with(Context context){
54        if (mImageLoader == null) {
55            synchronized (object) {
56                if (mImageLoader == null) {
57                    mImageLoader = new ImageLoader(context);
58                }
59            }
60        }
61        return mImageLoader;
62    }
63
64    private void initAsyncLoad() {
65        mExecutor = new ThreadPoolExecutor(CORE_THREAD_SIZE, THREAD_SIZE,
66                KEEP_ALIVE_TIME, TimeUnit.SECONDS,
67                new LinkedBlockingQueue<Runnable>(), new ThreadFactory() {
68            private final AtomicInteger count = new AtomicInteger(1);
69            @Override
70            public Thread newThread(Runnable runnable) {
71                return new Thread(runnable, "load bitmap thread "+ count.getAndIncrement());
72            }
73        });
74
75        mMainHandler = new Handler(Looper.getMainLooper()){
76            @Override
77            public void handleMessage(@NonNull Message msg) {
78                LoadResult result = (LoadResult) msg.obj;
79                ImageView imageView = result.imageView;
80                Bitmap bitmap = result.bitmap;
81                String url = result.url;
82                if (imageView == null || bitmap == null) {
83                    return;
84                }
85
86                //此判断是 避免 ImageView在列表中复用导致图片错位的问题
87                if (url.equals(imageView.getTag(VIEW_TAG_URL))) {
88                    imageView.setImageBitmap(bitmap);
89                }else {
90                    Log.w(TAG, "handleMessage: set image bitmap,but url has changed,ignore!");
91                }
92            }
93        };
94    }
95
96    private void initLruCache() {
97
98        mLruCache = new LruCache<String, Bitmap>((int) MEMORY_CACHE_MAX_SIZE){
99            @Override
100            protected int sizeOf(String key, Bitmap value) {
101                //缓存对象bitmap的大小,单位要和MEMORY_CACHE_MAX_SIZE一致
102                return value.getByteCount();
103            }
104
105            @Override
106            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
107                //移除旧缓存时会调用,可以在这里进行像资源回收的工作。
108            }
109        };
110
111    }
112
113    private void initDiskLruCache() {
114
115        File externalCacheDir = mApplicationContext.getExternalCacheDir();
116        if (externalCacheDir != null) {
117            long usableSpace = externalCacheDir.getUsableSpace();
118            if (usableSpace < DISK_LRU_CACHE_MAX_SIZE){
119                //剩余空间不够了
120                Log.e(TAG, "initDiskLruCache: "+"UsableSpace="+usableSpace+" , not enough(target 50M),cannot creat diskLruCache!");
121                return;
122            }
123        }
124
125        //一、创建DiskLruCache
126            //第一个参数是要存放的目录,这里选择外部缓存目录(若app卸载此目录也会删除);
127            //第二个是版本一般设1;第三个是缓存节点的value数量一般也是1;
128            //第四个是最大缓存容量这里取50M
129        try {
130            this.mDiskLruCache = DiskLruCache.open(mApplicationContext.getExternalCacheDir(), 11, DISK_LRU_CACHE_MAX_SIZE);
131        } catch (IOException e) {
132            e.printStackTrace();
133            Log.e(TAG, "initDiskLruCache: "+e.getMessage());
134        }
135    }
136
137    /**
138     * 缓存bitmap到内存
139     * @param url url
140     * @param bitmap bitmap
141     */

142    private void addBitmapMemoryCache(String url, Bitmap bitmap) {
143        String key = UrlKeyTransformer.transform(url);
144        if (mLruCache.get(key) == null && bitmap != null) {
145            mLruCache.put(key,bitmap);
146        }
147    }
148
149    /**
150     * 从内存缓存加载bitmap
151     * @param url url
152     * @return
153     */

154    private Bitmap loadFromMemoryCache(String url) {
155        return mLruCache.get(UrlKeyTransformer.transform(url));
156    }
157
158
159    /**
160     * 从磁盘缓存加载bitmap(并添加到内存缓存)
161     * @param url url
162     * @param requestWidth 要求的宽
163     * @param requestHeight 要求的高
164     * @return bitmap
165     */

166    private Bitmap loadFromDiskCache(String url, int requestWidth, int requestHeight) throws IOException {
167        if (Looper.myLooper()==Looper.getMainLooper()) {
168            Log.w(TAG, "loadFromDiskCache from Main Thread may cause block !");
169        }
170
171        if (mDiskLruCache == null) {
172            return null;
173        }
174        DiskLruCache.Snapshot snapshot = null;
175        String key = UrlKeyTransformer.transform(url);
176        snapshot = mDiskLruCache.get(key);
177        if (snapshot != null) {
178            FileInputStream inputStream = (FileInputStream)snapshot.getInputStream(0);
179
180            //Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
181
182            //一般需要采样加载,但文件输入流是有序的文件流,采样时两次decodeStream影响文件流的位置属性,
183            //导致第二次decode是获取是null,为解决此问题,可用文件描述符。
184            FileDescriptor fd = inputStream.getFD();
185            Bitmap bitmap = BitmapSampleDecodeUtil.decodeFileDescriptor(fd, requestWidth, requestHeight);
186            addBitmapMemoryCache(url,bitmap);
187            return bitmap;
188        }
189
190        return null;
191    }
192
193
194    /**
195     * 从网路加载图片 到磁盘缓存(然后再从磁盘中采样加载)
196     * @param urlString urlString
197     * @param requestWidth 要求的宽
198     * @param requestHeight 要求的高
199     * @return Bitmap
200     */

201    private Bitmap loadFromHttp(String urlString, int requestWidth, int requestHeight) throws IOException {
202        //线程检查,不能是主线程
203        if (Looper.myLooper()==Looper.getMainLooper()) {
204            throw new RuntimeException("Do not loadFromHttp from Main Thread!");
205        }
206
207        if (mDiskLruCache == null) {
208            return null;
209        }
210
211        DiskLruCache.Editor editor = null;
212        editor = mDiskLruCache.edit(UrlKeyTransformer.transform(urlString));
213        if (editor != null) {
214            OutputStream outputStream = editor.newOutputStream(0);
215            if (downloadBitmapToStreamFromHttp(urlString, outputStream)) {
216                editor.commit();
217            }else {
218                editor.abort();
219            }
220            mDiskLruCache.flush();
221        }
222
223        return loadFromDiskCache(urlString, requestWidth, requestHeight);
224    }
225
226    /**
227     * 从网络下载图片到文件输入流
228     * @param urlString
229     * @param outputStream
230     * @return
231     */

232    private boolean downloadBitmapToStreamFromHttp(String urlString, OutputStream outputStream) {
233        URL url = null;
234        HttpURLConnection urlConnection = null;
235        BufferedInputStream in = null;
236        BufferedOutputStream out = null;
237        try {
238            url = new URL(urlString);
239            urlConnection = (HttpURLConnection) url.openConnection();
240            in = new BufferedInputStream(urlConnection.getInputStream());
241            out = new BufferedOutputStream(outputStream);
242
243            int b;
244            while ((b=in.read()) != -1) {
245                //写入文件输入流
246                out.write(b);
247            }
248            return true;
249        } catch (IOException e) {
250            e.printStackTrace();
251            Log.e(TAG, "downloadBitmapToStreamFromHttp,failed : "+e.getMessage());
252        }finally {
253            if (urlConnection != null) {
254                urlConnection.disconnect();
255            }
256            IoUtil.close(in);
257            IoUtil.close(out);
258        }
259        return false;
260    }
261
262    /**
263     * 从网络直接下载bitmap(无缓存、无采样)
264     * @param urlString
265     * @return
266     */

267    private Bitmap downloadBitmapFromUrlDirectly(String urlString) {
268        URL url;
269        HttpURLConnection urlConnection = null;
270        BufferedInputStream bufferedInputStream = null;
271        try {
272            url = new URL(urlString);
273            urlConnection = (HttpURLConnection) url.openConnection();
274            bufferedInputStream = new BufferedInputStream(urlConnection.getInputStream());
275            return BitmapFactory.decodeStream(bufferedInputStream);
276        } catch (IOException e) {
277            e.printStackTrace();
278            Log.e(TAG, "downloadBitmapFromUrlDirectly,failed : "+e.getMessage());
279        }finally {
280            if (urlConnection != null) {
281                urlConnection.disconnect();
282            }
283            IoUtil.close(bufferedInputStream);
284        }
285        return null;
286    }
287
288    public Bitmap loadBitmap(String url){
289        return loadBitmap(url,0,0);
290    }
291
292    /**
293     * 同步 加载bitmap
294     *
295     * 不能在主线程执行。加载时 先从内存缓存获取,有就返回bitmap,若没有就从磁盘缓存获取;
296     * 磁盘缓存有就返回bitmap并缓存到内存缓存,没有就请求网络;
297     * 网络请求回来,就缓存到磁盘缓存,然后从磁盘缓存获取返回。
298     *
299     * @return Bitmap
300     */

301    public Bitmap loadBitmap(String url, int requestWidth, int requestHeight){
302
303        Bitmap bitmap = loadFromMemoryCache(url);
304        if (bitmap != null) {
305            Log.d(TAG, "loadBitmap: loadFromMemoryCache, url:"+url);
306            return bitmap;
307        }
308
309        try {
310            bitmap = loadFromDiskCache(url, requestWidth, requestHeight);
311        } catch (IOException e) {
312            e.printStackTrace();
313        }
314        if (bitmap != null) {
315            Log.d(TAG, "loadBitmap: loadFromDiskCache, url:"+url);
316            return bitmap;
317        }
318
319        try {
320            bitmap = loadFromHttp(url, requestWidth, requestHeight);
321        } catch (IOException e) {
322            e.printStackTrace();
323        }
324        if (bitmap != null){
325            Log.d(TAG, "loadBitmap: loadFromHttp, url:"+url);
326            return bitmap;
327        }
328
329        if (mDiskLruCache == null) {
330            Log.d(TAG, "loadBitmap: diskLruCache is null,load bitmap from url directly!");
331            bitmap = downloadBitmapFromUrlDirectly(url);
332        }
333
334        return bitmap;
335    }
336
337    public void loadBitmapAsync(final String url, final ImageView imageView){
338        loadBitmapAsync(url,imageView,0,0);
339    }
340
341    /**
342     * 异步 加载bitmap
343     * 外部可在任意线程执行,因为内部实现是在子线程(线程池)加载,
344     * 并且内部会通过Handler切到主线程,只需要传入view,内部就可直接绘制Bitmap到view。
345     * @param url
346     * @param imageView
347     * @param requestWidth
348     * @param requestHeight
349     */

350    public void loadBitmapAsync(final String url, final ImageView imageView, final int requestWidth, final int requestHeight){
351        if (url == null || url.isEmpty() || imageView == null) {
352            return;
353        }
354
355        // 标记当前imageView要绘制图片的url
356        imageView.setTag(VIEW_TAG_URL, url);
357
358        mExecutor.execute(new Runnable() {
359            @Override
360            public void run() {
361
362                Bitmap loadBitmap = loadBitmap(url, requestWidth, requestHeight);
363
364                Message message = Message.obtain();
365                message.obj = new LoadResult(loadBitmap, url, imageView);
366                mMainHandler.sendMessage(message);
367            }
368        });
369    }
370
371}
1/**
2 * Bitmap采样压缩加载工具
3 * @author hufeiyang
4 */

5public class BitmapSampleDecodeUtil {
6
7    private static final String TAG = "BitmapSampleDecodeUtil";
8
9    /**
10     * 对资源图片的采样
11     * @param resources resources
12     * @param resourcesId 资源id
13     * @param requestWidth view的宽
14     * @param requestHeight view的高
15     * @return 采样后的bitmap
16     */

17    public static Bitmap decodeSampleResources(Resources resources, int resourcesId, int requestWidth, int requestHeight){
18        if (resources == null || resourcesId<=0) {
19            return null;
20        }
21
22        //1、inJustDecodeBounds设为true,并加载图片
23        BitmapFactory.Options options = new BitmapFactory.Options();
24        options.inJustDecodeBounds = true;
25        BitmapFactory.decodeResource(resources, resourcesId, options);
26
27        //2、获取原始宽高信息
28        int outWidth = options.outWidth;
29        int outHeight = options.outHeight;
30
31        //3、原始宽高信息 和 view的大小 计算并设置采样率
32        options.inSampleSize = getInSampleSize(requestWidth, requestHeight, outWidth, outHeight);
33
34        //4、inJustDecodeBounds设为false,并加载图片
35        options.inJustDecodeBounds = false;
36
37        return BitmapFactory.decodeResource(resources, resourcesId, options);
38    }
39
40    /**
41     * 对文件描述符的采样加载
42     * @param fileDescriptor fileDescriptor
43     * @param requestWidth view的宽
44     * @param requestHeight view的高
45     * 注意,文件输入流是有序的文件流,采样时两次decodeStream影响文件流的文职属性,导致第二次decode是获取是null。
46     * 为解决此问题,可用本方法对文件流的文件描述符 加载。
47     */

48    public static Bitmap decodeFileDescriptor(FileDescriptor fileDescriptor, int requestWidth, int requestHeight){
49
50        if (fileDescriptor == null) {
51            return null;
52        }
53
54        BitmapFactory.Options options = new BitmapFactory.Options();
55        options.inJustDecodeBounds=true;
56        BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
57        options.inSampleSize = getInSampleSize(requestWidth, requestHeight, options.outWidth, options.outHeight);
58        options.inJustDecodeBounds = false;
59        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
60    }
61
62    /**
63     * 计算采样率
64     * @param width view的宽
65     * @param height view的高
66     * @param outWidth 图片原始的宽
67     * @param outHeight 图片原始的高
68     * @return
69     */

70    private static int getInSampleSize(int width, int height, int outWidth, int outHeight) {
71        int inSampleSize = 1;
72
73        if (width==0 || height ==0){
74            return inSampleSize;
75        }
76
77        if (outWidth>width || outHeight>height){
78            int halfWidth = outWidth / 2;
79            int halfHeight = outHeight / 2;
80            //保证采样后的宽高都不小于目标快高,否则会拉伸而模糊
81            while (halfWidth/inSampleSize >=width
82                    && halfHeight/inSampleSize>=height){
83                inSampleSize *=2;
84            }
85        }
86
87        Log.d(TAG, "getInSampleSize: inSampleSize="+inSampleSize);
88        return inSampleSize;
89    }
90}
1/**
2 * 图片的url转成key
3 * @author hufeiyang
4 */

5public class UrlKeyTransformer {
6
7    /**
8     * 图片的url转成key
9     * @param url
10     * @return MD5转换后的key
11     */

12    public static String transform(String url) {
13        if (url == null || url.isEmpty()) {
14            return null;
15        }
16        try {
17            MessageDigest digest = MessageDigest.getInstance("MD5");
18            return byteToHexString(digest.digest(url.getBytes()));
19        } catch (NoSuchAlgorithmException e) {
20            e.printStackTrace();
21        }
22        return null;
23    }
24
25    private static String byteToHexString(byte[] bytes) {
26        StringBuffer stringBuffer = new StringBuffer();
27        for (int i = 0; i < bytes.length; i++) {
28            String hex = Integer.toHexString(0XFF & bytes[i]);
29            if (hex.length()==1) {
30                stringBuffer.append(0);
31            }
32            stringBuffer.append(hex);
33        }
34        return stringBuffer.toString();
35    }
36}
1public class LoadResult {
2
3    public Bitmap bitmap;
4
5    /**
6     * bitmap对应的url
7     */

8    public String url;
9
10    public ImageView imageView;
11
12
13    public LoadResult(Bitmap bitmap, String url, ImageView imageView) {
14        this.bitmap = bitmap;
15        this.url = url;
16        this.imageView = imageView;
17    }
18}


具体代码gitHub地址:SimpleImageLoader (点击“阅读原文”查看)




推荐阅读:

子线程 真的不能更新UI ?

必要掌握!Window、WindowManager !

带你彻底搞懂-View的工作原理!





点个在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存