一、基础
Bitmap指的是一张图片,可以是png格式,也可以是jpg等其它常见的图片格式。
1.加载方式
Android中通常使用BitmapFactory来加载Bitmap,方法如下:
BitmapFactory.decodeByteArray() //从字节数组加载
BitmapFactory.decodeFile() //从文件加载
BitmapFactory.decodeFileDescriptor() //从"文件描述符"加载
BitmapFactory.decodeResource() //从资源加载,通常为drawable资源
BitmapFactory.decodeStream() //从输入流加载
2.内存位置
- Android 3.0之前:Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中。
- Android 3.0到Android 7.1:Bitmap的内存就全部在Dalvik Heap中。
- Android 8.0及之后,Bitmap的像素数据又重新回到Native分配了。
3.内存回收
- Android 3.0之前,使用Bitmap.recycle()进行Bitmap的内存回收。
- Android 3.0到Android 7.1,不需要手动回收Bitmap。
- Android 8.0及之后,不需要手动回收Bitmap。
4.内存复用(inBitmap)
- Android 3.0开始,在Bitmap中引入了一个新的字段BitmapFactory.Options.inBitmap,设置此字段为true后,解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程。
- Android4.4(API 19)之前只有格式为jpg或png、同等宽高、inSampleSize为1的Bitmap才可以复用。
- Android4.4(API 19)及之后被复用的Bitmap的内存大于或等于需要新申请内存的Bitmap的内存就可以复用了。
用法(和LruCache配合实现内存的两级缓存):
(1)将需要缓存的图片存入LruCache,当LruCache删除不用的图片时,将删除的图片放入软引用中。
protected synchronized void entryRemoved(boolean evicted, Object key, BitmapDrawable oldValue, BitmapDrawable newValue) {
//reusableBitmaps是HashMap<Object, SoftReference<Bitmap>>用于缓存从LruCache中移除的Bitmap
reusableBitmaps.put(key, new SoftReference<Bitmap>(oldValue.getBitmap()));
}
(2)在加载图片时,先调用LruCache.get(key),如果拿不到Bitmap对象再调用reusableBitmaps.get(key)进行获取。
5.像素格式
- ALPHA_8:颜色信息只由透明度组成,占8位。
- ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。
- ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。该像素格式是Bitmap默认的颜色配置信息,也是最占空间的一种配置。
- RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。
通常我们优化Bitmap时,当需要做性能优化或者防止OOM,我们通常会使用RGB_565,因为ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。
6.内存计算
(1)获取Bitmap内存大小方法
getByteCount():
API 12加入,表示存储Bitmap的像素需要的最少内存。
getAllocationByteCount():
API 19加入,用于代替getByteCount(),表示为Bitmap分配的实际内存大小。
一般情况下getByteCount()和getAllocationByteCount()是相等的。但是Bitmap内存如果复用之后,两者就不一样了。通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用的内存大小(并非实际内存大小,实际内存大小是被复用Bitmap的内存大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小。
例子:(inBitmap)
- 三星G9500,屏幕像素密度480
- res/drawable/ic_launcher2,实际图片分辨率:192*192
- res/drawable/ic_launcher,实际图片分辨率:72*72
- drawable目录对应的屏幕像素密度为160
//测试代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher2, options);
Log.d(TAG, "zwm, bitmap = " + bitmap);
Log.d(TAG, "zwm, bitmap width: " + bitmap.getWidth());
Log.d(TAG, "zwm, bitmap height: " + bitmap.getHeight());
Log.d(TAG, "zwm, bitmap getByteCount: " + bitmap.getByteCount());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(TAG, "zwm, bitmap getAllocationByteCount: " + bitmap.getAllocationByteCount());
}
options.inMutable = true;
options.inBitmap = bitmap;
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher, options);
Log.d(TAG, "zwm, bitmap2 = " + bitmap2);
Log.d(TAG, "zwm, bitmap2 width: " + bitmap2.getWidth());
Log.d(TAG, "zwm, bitmap2 height: " + bitmap2.getHeight());
Log.d(TAG, "zwm, bitmap2 getByteCount: " + bitmap2.getByteCount());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(TAG, "zwm, bitmap2 getAllocationByteCount: " + bitmap2.getAllocationByteCount());
}
//输出log
2019-12-23 16:28:46.137 zwm, bitmap = android.graphics.Bitmap@6497778 //Bitmap对象
2019-12-23 16:28:46.137 zwm, bitmap width: 576
2019-12-23 16:28:46.138 zwm, bitmap height: 576
2019-12-23 16:28:46.138 zwm, bitmap getByteCount: 1327104
2019-12-23 16:28:46.138 zwm, bitmap getAllocationByteCount: 1327104
2019-12-23 16:28:46.141 zwm, bitmap2 = android.graphics.Bitmap@6497778 //对象同上
2019-12-23 16:28:46.141 zwm, bitmap2 width: 216
2019-12-23 16:28:46.141 zwm, bitmap2 height: 216
2019-12-23 16:28:46.141 zwm, bitmap2 getByteCount: 186624 //新解码图片占用的内存大小
2019-12-23 16:28:46.141 zwm, bitmap2 getAllocationByteCount: 1327104 //被复用Bitmap真实占用的内存大小
(2)资源文件夹下加载图片得到Bitmap内存大小计算方法
7.采样(inSampleSize)
public class ImageResizer {
public ImageResizer(){}
/**
* 从资源文件中获取相应的图片
* @param res
* @param resId
* @param reqWidth
* @param reqHeight
* @return
*/
public Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
Options options = new Options();
//设置只加载宽高标志位
options.inJustDecodeBounds = true;
//加载原始图片宽高到Options中
BitmapFactory.decodeResource(res, resId, options);
//通过所需图片宽高和原始图片宽高计算采样率
options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
//还原并再次加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* 从文件描述符中获取相应的图片
* @param reqWidth
* @param reqHeight
* @param options
* @return
*/
public Bitmap decodeSampleBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
//计算采样率
public static int calculateSampleSize(int reqWidth, int reqHeight, Options options) {
//如果传入0参数,则将采样率设成1,即不压缩
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
int inSampleSize = 1;
int width = options.outWidth;
int height = options.outHeight;
//当所需宽高比实际宽高小时才进行压缩
if(reqWidth < width && reqHeight < height) {
int halfWidth = width / 2;
int halfHeight = height / 2;
//保证压缩后的宽高不能小于所需宽高
while(reqWidth <= halfWidth && reqHeight <= halfHeight) {
inSampleSize *= 2;
halfWidth /= 2;
halfHeight /= 2;
}
}
return inSampleSize;
}
}
8.缩放(inScaled)
- 如果inScaled设置为true就缩放,设置为false则不缩放,默认值是true。
- 缩放只针对资源文件有效,对于其他来源的图片不起效果。
- 缩放的比例为:inTargetDensity / inDensity。
二、缓存策略
Android中三级缓存策略:内存 -- 磁盘 -- 网络。在获取资源时先从内存缓存中获取,如果没有则从磁盘缓存中获取,如果还是没有再从网络中获取。Android中通过LruCache实现内存缓存,通过DiskLruCache实现磁盘缓存,它们采用的都是LRU(Least Recently Used)最近最少使用算法来移除缓存。
1.LruCache
LruCache是Android提供的一个缓存类,它是一个泛型类,内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加,当缓存满时,会移除较早使用的缓存对象,然后再添加新的缓存对象。
(1)强引用、弱引用、软引用、虚引用
强引用(StrongReference):
可以通过以下方式创建一个强引用对象:
String str = "abc";
变量str就是字符串对象"abc"的一个强引用。
如下代码会移除str的强引用:
str = null;
此时垃圾回收器就会在某一时刻回收该对象。
强引用可以阻止垃圾回收器回收对象。
弱引用(WeakReference):
可以通过以下方式创建一个弱引用对象:
String str = "test";
WeakReference<String> wr = new WeakReference<String>(str);
str = null;
如果一个对象有弱引用指向它,当移除强引用时,垃圾回收器会立即回收该对象。
弱引用无法阻止垃圾回收器回收对象。
软引用(SoftReference):
可以通过以下方式创建一个软引用对象:
String str = "test";
SoftPreference sr = new SoftPreference(str);
str = null;
如果一个对象有软引用指向它,当移除强引用时,对象不会立即被回收,只有在JVM需要内存时,才会回收该对象。
软引用无法阻止垃圾回收器回收对象,但可以延迟回收。
虚引用(PhantomReference):
可以通过以下方式创建一个虚引用对象:
String str = "test"
PhantomReference pr = new PhantomReference(str);
str = null;
如果一个对象有虚引用指向它,当移除强引用时,拥有虚引用的对象可以在任何时刻被垃圾回收器回收。
虚引用无法阻止垃圾回收器回收对象。
(2)LruCache使用
//初始化LruCache对象
public void initLruCache() {
//获取当前进程的可用内存,转换成KB单位
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//分配缓存的大小
int maxSize = maxMemory / 8;
//创建LruCache对象并重写sizeOf方法
lruCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
// TODO Auto-generated method stub
return value.getWidth() * value.getHeight() / 1024;
}
};
}
/**
* 将图片存入缓存
* @param key 图片的url转化成的key
* @param bitmap
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if(getBitmapFromMemoryCache(key) == null) {
mLruCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemoryCache(String key) {
return mLruCache.get(key);
}
/**
* 因为外界一般获取到的是url而不是key,因此再做一层封装
* @param url http url
* @return bitmap
*/
private Bitmap loadBitmapFromMemoryCache(String url) {
final String key = hashKeyFromUrl(url);
return getBitmapFromMemoryCache(key);
}
2.DiskLruCache
DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存。DiskLruCache得到了Android官方文档的推荐,但它不属于Android SDK的一部分。
(1)DiskLruCache使用
//初始化DiskLruCache,包括一些参数的设置
public void initDiskLruCache {
//配置固定参数
// 缓存空间大小
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
//下载图片时的缓存大小
private static final long IO_BUFFER_SIZE = 1024 * 8;
// 缓存空间索引,用于Editor和Snapshot,设置成0表示Entry下面的第一个文件
private static final int DISK_CACHE_INDEX = 0;
//设置缓存目录
File diskLruCache = getDiskCacheDir(mContext, "bitmap");
if(!diskLruCache.exists())
diskLruCache.mkdirs();
//创建DiskLruCache对象,当然是在空间足够的情况下
if(getUsableSpace(diskLruCache) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskLruCache,
getAppVersion(mContext), 1, DISK_CACHE_SIZE);
mIsDiskLruCache = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 从网络中下载图片到磁盘中并获取到按需压缩后的图片
* 1.由于涉及到网络通信,因此该方法应该运行在子线程当中
* 2.图片是完整下载下来的,reqWidth和reqHeight只是在从磁盘读取的时候进行压缩用的
* 3.在存到磁盘的时候url是转换过的编码的
* 4.要通过editor的commit保证被其它的Reader看到,而且在commit中会保证缓存大小不超过阈值
* 5.最后记得调用flush()将数据确实写入文件系统
*
* @param url 图片网址
* @param reqWidth 所需宽度
* @param reqHeight 所需高度
* @return 按需压缩后的图片
* @throws IOException
*/
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
if(Looper.myLooper() == Looper.getMainLooper())
throw new RuntimeException("can not visit network from UI Thread.");
if(mDiskLruCache == null)
return null;
String key = hashKeyForDisk(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
//写入完成后绝不能忘了commit和flush
if(downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
//flush确保数据写入文件系统
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
/**
* 考虑到直接使用URL作为DiskLruCache中LinkedHashMap的Key不太适合,
* 因为图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。
* 其实最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,
* 并且只会包含0-F这样的字符,完全符合文件的命名规则。
*
* 该方法在写入磁盘和从磁盘读取时都需要用到,因为它是计算出索引的方法
*
* @param key 图片的url
* @return MD5编码之后的key
*/
public String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
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();
}
/**
* 将图片资源写到文件系统上
* @param urlString 资源
* @param outputStream Editor的对应Entry下的文件的OutputStream
* @return
*/
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),
IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 磁盘缓存的读取
* @param url
* @param reqWidth
* @param reqHeight
* @return
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
if(Looper.myLooper() == Looper.getMainLooper())
Log.w(TAG, "it's not recommented load bitmap from UI Thread");
if(mDiskLruCache == null)
return null;
Bitmap bitmap = null;
String key = hashKeyForDisk(url);
Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fd = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
if(bitmap != null)
addBitmapToMemoryCache(key, bitmap);
}
return bitmap;
}
三、ImageLoader
ImageLoader封装了Bitmap的高效加载、缓存策略(LruCache和DiskLruCache),应该具备以下功能:
1.图片的同步加载
/**
* load bitmap from memory cache or disk or network
* NOTE that should run in a new Thread
* @param url http url
* @param reqWidth the width that imageView desire
* @param reqHeight the height that imageView desire
* @return bitmap maybe null
*/
public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemoryCache(url);
if(bitmap != null) {
Log.d(TAG, "loadBitmapFromMemoryCache, url:"+url);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
if(bitmap != null) {
Log.d(TAG, "loadBitmapFromDiskCache, url:"+url);
return bitmap;
}
bitmap = loadBitmapFromHttp(url, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp, url:"+url);
} catch (Exception e) {
e.printStackTrace();
}
if(bitmap == null && !mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(url);
}
return bitmap;
}
2.图片的异步加载
/**
* load bitmap from memory cache or disk or http,
* then bind bitmap and ImageView
* @param uri
* @param imageView
*/
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}
/**
* 封装了从url获取图片到加载到特定的ImageView当中的方法
* 先从内存中获取图片,如果获取不到则调用loadBitmap获取图片
* 如果是从网络中获取图片,则在将图片加载到ImageView前,会将图片的url和ImageView的tag进行对比:
* (1)如果相同则说明ImageView没有被复用,可以加载图片;
* (2)如果不同则说明ImageView被复用了,放弃加载下载完成的图片。
*
* @param url
* @param imageView
* @param reqWidth
* @param reqHeight
*/
public void bindBitmap(final String url, final ImageView imageView,
final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI);
Bitmap bitmap = loadBitmapFromMemoryCache(url);
if(bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
if(bitmap != null) {
LoaderResult result = new LoaderResult(url, imageView, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result)
.sendToTarget();
}
};
};
THREAD_POOL_EXEUTOR.execute(loadBitmapTask);
}
3.高效加载
- 采样
- 像素格式
4.内存缓存
- LruCache
- 内存复用
5.磁盘缓存
- DiskLruCache