如何有效的加载一个 Bitmap
原因是 Android 对单个应用所施加的内存限制最大是 16MB,超出后就会出现内存溢出 OOM。
Bitmap 在 Android 中指的是一张图片,那么如何加载一张图片呢?BimapFactory类
提供了四类方法:decodeFile
,decodeResoure
,decodeStream
,decodeByteArray
,分别对应的是从文件系统、资源、输入流以及字节数组中加载出一个 Bitmap 对象。其中 decodeFile
和decodeResource
有间接调用了decodeStream
方法,这四类方法最终是在Android的底层
实现,对应的是 BitmapFactory类
的几个native
方法。
如何高效加载 Bitmap 呢?
核心思想是采用
BitmapFactory.Options
来加载所需尺寸的图片,因为很多时候 ImageView 并没有图片的原始尺寸那么大,这时候直接把图片的原始尺寸设置给 ImageView 是没必要的,可以通过BitmapFactory.Options
按照一定的采样率
来加载缩小后的图片,然后在 ImageView 中显示,这样就会降低内存占用从而在一定程度上避免了OOM
,提高了 Bitmap 加载时的性能。
以通过BitmapFactory.Options
来缩放图片,主要是采用到了它的inSampleSize
参数,即采样率
。
当inSampleSize = 1
时采样后的图片为原始图片的大小
; 当inSampleSize > 1
时采样后的图片宽/高为原始图片的大小的 1/inSampleSize,而像素数为原图的 1/(inSampleSize的2次方)
;
当inSampleSize = 2
时采样后的图片为原始图片大小的 1/2
,像素数为原图的1/4
,内存占有大小也为原图的1/4
,拿一张 ARGB8888 格式的宽高1024 * 1024 的图片进行压缩,压缩前它的内容占用为1024 * 1024 * 4(该值由图片格式决定)
大小 ,当压缩时图片使用采样率为 inSampleSize = 2 去 压缩,那么采样后的图片占用内存为 512 * 512 * 4
大小。
当inSampleSize < 1
时其作用相当于1,即无压缩效果
。最新的官方文档中指出 inSampleSize 的取值应该总为 2 的指数
,当 inSampleSize 不为 2 的指数时系统会向下取整
并选择一个最接近2的指数
来代替,比如 3 系统会选择 2 来代替,但是这个结论并非在所有的 Android 版本上都成立,因此把它当成一个开发建议。
如何获取采样率?
- 将
BitmapFactory.Options
的inJustDecodeBounds
参数设置为 true 并加载图片 - 从
BitmapFactory.Options
中取出图片的原始宽高信息,他们对应于 outWidth 和 outHeight 参数 - 根据采样率的规则并结合木匾 View 的所需大小计算出采样率 inSampleSize
- 将
BitmapFactory.Options
的inJustDecodeBounds
参数设为 false,然后重新加载图片。
inJustDecodeBounds 参数:
当该参数设置为 true 时,BitmapFactory 只会解析图片的原始宽高信息
,并不会真正去加载图片,所以这个操作时轻量级的。
代码如下:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){
final BitmapFactory.Options options = new BitmapFactory.Options() ;
options.inJustDecodeBounds = true ;
BitmapFactory.decodeResource(res, resId, options);
// 重新加载
options.inJustDecodeBounds = false ;
return BitmpFactory.decodeResource(res, resId, options)
}
/**
* 计算采样率
*/
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight){
final int height = options.outHeight ;
final int width = options.outWidth ;
int inSampleSize = 1 ;
// 实际宽高大于指定宽高 才做压缩
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2 ;
final int halfWidth = width / 2 ;
while((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize >= reqWidth)){
inSampleSize *= 2 ;
}
}
return inSampleSize ;
}
比如 ImageView 所希望的图片大小为 100 * 100 像素
这时候可以通过如下方式高效的加载并显示图片:
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage,100,100)) ;
Android 中常用的缓存策略
缓存策略的一个通用思想,可以在很多场合使用,但是实际开发中经常需要使用的是 Bitmap 做缓存,通过缓存策略,我们就可以不用每次都从服务器或者存储设备中加载图片,这样就可以提高加载效率以及产品的用户体验。
目前比较常用的缓存策略是 LruCache( 最近最少使用算法 ) 和 DiskLruCache(磁盘最近最少使用算法)。其中 LruCache 常被用作内存缓存,DiskLruCache 常被用作存储缓存。
![](/qrcode.jpg)
最近最少使用算法:当缓存快满时,会淘汰近期最少使用的缓存目标。
了解一下三级缓存
三级缓存分别指的是内存缓存、磁盘缓存、网络缓存。第一次加载数据内存和磁盘中都没有,所以先从网络加载数据,然后将数据缓存到磁盘和内存中。当下次再次加载已缓存的数据时就不用再去请求网络,直接从磁盘或者内存中读取,然后显示。上述的缓存策略也适用于其他文件类型。
缓存策略主要包含缓存的添加、获取和删除。而缓存的删除主要是因为存储设备的容量大小限制,当缓存容量达到存储设备的最大值,就需要删除旧的缓存。那么如何判断那些属于旧的缓存就需要更好的算法来计算了。目前常用的缓存算法是 LRU(Least Recently Used)
,最近最少使用算法。
缓存的添加
加载网络图片,然后缓存到本地。
(1)获取到网络图片 url
(2)将图片url
转为MD5
格式,作为key
存储
new Thread(new Runnable() {
@Override
public void run() {
try {
String imageUrl = "http://ww3.sinaimg.cn/large/0066P23Wjw1f7efqelrh4j30300300si.jpg";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null){
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(key,outputStream)){
editor.commit();
}else {
editor.abort();
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
/**
* 将字符串进行MD5编码
* 因为 url 和 key 要一一对应,而如果拿 url 作 key 还是有可能重复的,
* 所以转为 MD5 就不会重复了
* @param key
* @return
*/
public String hashKeyForDisk(String key){
String cacheKey ;
try {
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest()) ;
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode()) ;
}
return cacheKey ;
}
/**
* 将字节转为字符
* @param bytes
* @return
*/
private String bytesToHexString(byte[] bytes){
StringBuilder sb = new StringBuilder();
for (int i=0; i < bytes.length; i++){
String hex = Integer.toHexString(0xFF & bytes[i]) ;
if (hex.length() == 1){
sb.append('0') ;
}
sb.append(hex) ;
}
return sb.toString() ;
}
/**
* 将 url 转为流,成功返回true,失败返回 false
* @param urlString
* @param outputStream
* @return
*/
private boolean downloadUrlToStream(String urlString, OutputStream outputStream){
HttpURLConnection urlConnection = null ;
BufferedInputStream in = null ;
BufferedOutputStream out = null ;
try {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = in.read()) != -1){
out.write(b);
}
return true ;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (urlConnection != null){
urlConnection.disconnect();
}
try {
if (out != null){
out.close();
}
if (in != null){
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false ;
}
缓存的获取
(1)获取到url
对应的 key
(2)读取流
try {
// 读取缓存
String imageUrl = "http://ww3.sinaimg.cn/large/0066P23Wjw1f7efqelrh4j30300300si.jpg";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
Log.e("222", "onCreate: ----->"+snapShot);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
imageView.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
缓存的删除
try {
String imageUrl = "http://ww3.sinaimg.cn/large/0066P23Wjw1f7efqelrh4j30300300si.jpg";
String key = hashKeyForDisk(imageUrl);
mDiskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
优化列表的卡顿现象
ListView 和 GridView 由于要加载大量的子视图,当用户滑动的时候就容易出现卡顿的现象,所以我们要对列表的卡顿现象进行优化。
- 对列表进行复用
- 不在
getView
中做耗时操作 - 在列表停止时加载数据
- 开启硬件加速
参考
Android 开发艺术探索
郭霖大神博客
Android DiskLruCache完全解析,硬盘缓存的最佳方案