Bitmap的加载和Cache
“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也提供了对应的方法,步骤如下:
把BitmapFactory.Options.inJustDecodeBounds设为true,并加载图片。(只加载原始宽高信息,轻量级操作)
获取原始宽高信息:options.outWidth、options.outHeight
设置采样率options.inSampleSize。采样率根据 原始宽高信息 和 view的大小计算。
把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>(0, 0.75f, true);
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(), 1, 1, 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内部线程异步执行
说明,
”三级缓存“的逻辑:加载时 先从内存缓存获取,有就返回bitmap绘制图片到view,若没有就从磁盘缓存获取;磁盘缓存有就返回bitmap并缓存到内存缓存,没有就请求网络;网络请求回来,就缓存到磁盘缓存,然后从磁盘缓存获取返回。
同步加载,是在外部的子线程中执行,同步加载方法内部没有开线程,所以加载过程是耗时的 会阻塞 外部的子线程,加载完成后 需要自行切到主线程绘制到view。
异步加载,外部可在任意线程执行,因为内部实现是在子线程(线程池)加载,并且内部会通过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(), 1, 1, 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 (点击“阅读原文”查看)
推荐阅读:
点个在看吧