1、背景
在ImageView里面加载一张远端的Url图片是我们在搭建页面时经常用到的功能,比较成熟的图片加载控件有ImageLoader,Fresco.
但如果我们希望简单的实现加载Url图片的功能,而不想引入这些较重的组件我们该怎么做呢?
2、Url图片加载器基本功能
- 图片Url转InputStream
- InputStream转Bitmap,并按比例压缩
- 内存缓存和内存释放
- 多线程和并发处理
2.1、图片Url转InputStream
使用UrlConnection,代码示例如下:
1 2 3 4 5 |
URL myFileUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection)myFileUrl.openConnection(); connection.setDoInput(true); connection.connect(); iStream = connection.getInputStream(); |
2.2、InputStream转Bitmap,并按比例压缩
来自网络的url图片可能大小和尺寸较大,直接加载到本地可能会导致内存溢出。而我们真正使用的时候是放在某个ImageView中,这个ImageView的大小就决定了我们想要的图片的尺寸。所以我们可以不需要原始尺寸的图片,这里我们需要用到图片压缩,具体使用到:
1 |
Options.inJustDecodeBounds=true |
设置Options.inJustDecodeBounds=true,这时候decode的bitmap为null, 只是把图片的宽高放在Options里, 然后第二步就是设置合适的压缩比例inSampleSize,这时候获得合适的Bitmap。
这里我们需要外部传入一个期望的width,如果解析Url图片发现原始的图片大小比期望的大,我们会按照这个传入的width计算出一个缩放比例,这个比例就是第二步解析需要设置的inSampleSize。代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
BitmapFactory.Options outOptions = new BitmapFactory.Options(); // 设置该属性为true,不加载图片到内存,只返回图片的宽高到options中。 outOptions.inJustDecodeBounds = true; // 加载获取图片的宽高 BitmapFactory.decodeStream(inputStream, null, outOptions); if (outOptions.outWidth > width) { // 根据宽设置缩放比例 outOptions.inSampleSize = outOptions.outWidth / width; outOptions.outWidth = width; // 计算缩放后的高度 int height = outOptions.outHeight / outOptions.inSampleSize; outOptions.outHeight = height; } // 重新设置该属性为false,加载图片返回 outOptions.inJustDecodeBounds = false; inputStream.close(); inputStream = getInputStream(url); bitmap = BitmapFactory.decodeStream(iStream, null, options); inputStream.close(); |
2.3、内存缓存和内存释放
通过上面两个步骤我们已经可以从网络下载一张图片,并转为合适的Bitmap了,但是如果没有内存缓存,那么每次都要走这两个步骤,非常消耗性能,且体验不好。我们需要加一个简单的缓存,我们这里用到LruCache。
2.3.1、LruCache Vs SoftReference/WeakReference
在LruCache广泛应用之前,SoftReference/WeakReference是占主导地位的图片out of memory解决方案,但是从android 3.0起,Google已经不在建议继续使用该方案,原因是:
In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache, however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective. In addition, prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash.
LruCache的使用示例如下:
1 2 3 4 5 6 7 8 |
int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024); int cacheSize = maxMemory/8; mMemoryCache = new LruCache<String,Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes()*value.getHeight()/1024; } }; |
我们需要实现sizeOf方法, 用来返回缓存中object的大小。设置缓存的时候使用如下代码:
1 2 |
String urlKey = url.hashCode() + "_" + width; cache.put(url, bmp); |
url.hashCode()主要是避免url太长导致不必要的内存消耗;width是用于区分同一个url不同宽高比例的缓存。
有了图片的缓存,我们同样需要关注内存的释放。LruCache是常驻内存的,我们需要在一个合适的时机释放掉图片内存,方式有二:
1、在Activity的onLowMemory和onTrimMemory方法中释放缓存
尽管系统在内存不足的时候杀进程的顺序是按照LRU Cache中从低到高来的,但是它同时也会考虑杀掉那些占用内存较高的应用来让系统更快地获得更多的内存。
所以如果你的应用占用内存较小,就可以增加不被杀掉的几率,从而快速地恢复(如果不被杀掉,启动的时候就是热启动,否则就是冷启动,其速度差在2~3倍)。
所以说在几个不同的OnTrimMemory回调中释放自己的UI资源,可以有效地提高用户体验。以下是onTrimMemory回调方法
- TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
- TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
- TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。
使用示例如下:
1 2 3 4 5 6 7 8 9 |
@Override public void onTrimMemory(int level) { if (level >= TRIM_MEMORY_MODERATE) { cache.evictAll(); } else if (level >= TRIM_MEMORY_BACKGROUND) { cache.trimToSize(cache.size() / 2); } } |

2、在业务合适的时机手动调用释放内存
如果无法获得onTrimMemory这些生命周期,我们可以在合适的时机(例如页面关闭的时候)调用缓存释放的方法。
2.4、多线程和并发处理
实际使用场景中同一张图片可能同时被多个线程加载,此时如果同一张图片被加载多次将是资源的消耗,我们需要处理这种多线程并发问题。
我们可以使用AsyncTask,AsyncTask 自带有两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler), 其中SerialExecutor是用来任务排队的 ,而线程池THREAD_POOL_EXECUTOR是用来真正执行任务的; AsyncTask异步任务底层是封装了线程池和Handler 。
而对于多个同样的图片请求,我们可以通过同步锁来保证只有一个真实的请求,其它的请求会共享这个请求的结果,代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 |
synchronized (mCallBackMap) { if(mCallBackMap.containsKey(cacheKey)) { Set<CallBack> callbacks = mCallBackMap.get(cacheKey); callbacks.add(callBack); return; } else { Set<CallBack> callBackSet = new HashSet<>(); callBackSet.add(callBack); mCallBackMap.put(cacheKey, callBackSet); } } |