现在市场上有不少封装好的第三方库,对Bitmap内存也是作到了很好的优化,好比Glide、Fresco,每次加载只要直接调用就好,可是除掉第三方库外,咱们仍是须要去了解一下Bitmap的基本优化手段。java
首先咱们有必要去了解一下Bitmap的基本知识点,在Android3.0以前,Bitmap的对象是放在Java堆中,而Bitmap的像素是放置在Native内存中,这个时候须要手动的去调用recycle,才能去回收Native内存;算法
在Android3.0到Android7.0,Bitmap对象和像素都是放置到Java堆中,这个时候即便不调用recycle,Bitmap内存也会随着对象一块儿被回收。虽然Bitmap内存能够很容易被回收,可是Java堆的内存有很大的限制,也很容易形成GC。缓存
在Android8.0的时候,Bitmap内存又从新放置到了Native中。markdown
Bitmap形成OOM不少时候也是由于对Bitmap的资源没有获得很好的利用,同时没有作到及时的释放。网络
对于Bitmap的优化主要分为针对不一样密度的设备合理的分配资源,压缩以及缓存处理三种。app
总所周知,drawable时放置本地图片资源的地方,从上图能够发现,AS将drawable分为了mdpi,hdpi,xhdpi...不一样的等级,简单归纳为不一样等级的dpi表明着不一样的设备密度,它们之间的区别暂时先不论,有必要先去了解一下AS对于drawable的匹配规则.ide
举个例子,当当前的设备密度为xhdpi,此时代码中ImageView须要去引用drawable中的图片,那么根据匹配规则,系统首先会在drawable-xhdpi文件夹中去搜索,若是须要的图片存在,那么直接显示;若是不存在,那么系统将会开始从更高dpi中搜索,例如drawable-xxhdpi,drawable-xxxhdpi,若是在高dpi中搜索不到须要的图片,那么就会去drawable-nodpi中搜索,有则显示,无则继续向低dpi,如drawable-hdpi,drawable-mdpi,drawable-ldpi等文件夹一级一级搜索.优化
当在比当前设备密度低的文件夹中搜到图片,那么在ImageView(宽高在wrap_content状态下)中显示的图片将会被放大.图片放大也就意味着所占内存也开始增多.这也就是为何分辨率不高的图片随意放置在drawable中也会出现OOM.而在高密度文件夹中搜到图片,图片在该设备上将会被缩小,内存也就相应减小.ui
在理想的状态下,不一样dpi的文件下应该放置相应dpi的图片资源,以对不一样的设备进行适配.但在图片资源没有作dpi区分的时候,根据以上所说的匹配规则,将图片资源放置在高dpi 如drawable-xdpi,drawable-xxdpi文件夹中.是比较好的选择,在最大程度上减小OOM的概率。this
当装载图片的容器例如ImageView只有100*100,而图片的分辨率为800 * 800,这个时候将图片直接放置在容器上,很容易OOM,同时也是对图片和内存资源的一种浪费。当容器的宽高都很小于图片的宽高,其实就须要对图片进行尺寸上的压缩,将图片的分辨率调整为ImageView宽高的大小,一方面不会对图片的质量有影响,同时也能够很大程度上减小内存的占用。
对于尺寸压缩首先须要去了解一个知识点inSampleSize,
从上图发现Android官方对它的解释是,若是inSampleSize 设置的值大于1,则请求解码器对原始的bitmap进行子采样图像,而后返回较小的图片来减小内存的占用,例如inSampleSize == 4,则采样后的图像宽高为原图像的1/4,而像素值为原图的1/16,也就是说采样后的图像所占内存也为原图所占内存的1/16;当inSampleSize <=1时,就看成1来处理也就是和原图同样大小。另外最后一句还注明,inSampleSize的值一直为2的幂,如1,2,4,8。任何其余的值也都是四舍五入到最接近2的幂。
采样率inSampleSize实际上是一个规定图片压缩倍数的一个参数,经过图片宽高的比较获得一个新的数值,inSampleSize设置到BitmapFactory中从新去解码图片。下面就是利用inSampleSize对图片进行尺寸上的优化代码。
/** * 对图片进行解码压缩。 * * @param resourceId 所需压缩的图片资源 * @param reqHeight 所需压缩到的高度 * @param reqWidth 所需压缩到的宽度 * @return Bitmap */
private Bitmap decodeBitmap(int resourceId, int reqHeight, int reqWidth) {
BitmapFactory.Options options = new BitmapFactory.Options();
//inJustDecodeBounds设置为true,解码器将返回一个null的Bitmap,系统将不会为此Bitmap上像素分配内存。
//只作查询图片宽高用。
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), resourceId, options);
//查询该图片的宽高。
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
//若是当前图片的高或者宽大于所需的高或宽,
// 就进行inSampleSize的2倍增长处理,直到图片宽高符合所须要求。
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while ((halfHeight / inSampleSize >= reqHeight)
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
//inSampleSize获取结束后,须要将inJustDecodeBounds置为false。
options.inJustDecodeBounds = false;
//返回压缩后的Bitmap。
return BitmapFactory.decodeResource(getResources(), resourceId, options);
}
复制代码
通常状况下质量压缩是不推荐的一种优化手法,此手法压缩后图片将会失真。但不排除有项目对图片的清晰度没有太高的要求。
在开始谈如何压缩以前咱们须要了解一下Bitmap的质量等级,在API29中,将Bitmap分为ALPHA_8, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE六个等级。
每一个等级每一个像素所占用的字节也都不同,所存储的色彩信息也不一样。同一张100像素的图片,ARGB_8888就占了400字节,RGB_565才占200字节,RGB_565在内存上取得了优点,可是Bitmap的色彩值以及清晰度却不如ARGB_8888模式下的Bitmap。质量压缩说到底就是用清晰度来换内存。
质量压缩的具体操做也和上面2.2同样,只是将options.inPreferredConfig 设置为所需的图片质量,以下:
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
复制代码
不论是从网络上下载图片,仍是直接从USB中读取图片,缓存对于图片加载的优化起到了相当重要的做用。当咱们首次从网络上或者USB读取图片,会对图片进行相应的压缩处理。在处理事后不加入缓存,下一次请求图片仍是直接从网络上或者USB中直接读取,不只消耗了用户的流量还重复对图片进行压缩处理,占用多余内存的同时加载图片也很缓慢。
对于缓存,目前的策略是内存缓存和存储设备缓存。当加载一张图片时,首先会从内存中去读取,若是没有就接着在存储设备中读,最后才直接从网络或者USB中读取。接下来就聊一聊这两种缓存的具体内容。
LRU是用于实现内存缓存的一种常见算法,LRU也叫作最近最少使用算法,通俗来说就是当缓存满了的时候,就会优先的去淘汰最近最少使用的缓存对象。接下来就以代码的方式直观的分析。
...
private LruCache<Integer,Bitmap> mCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1.初始化LruCache.
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mCache = new LruCache<Integer,Bitmap>(cacheSize){
@Override
protected int sizeOf(Integer key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
}
//2.从Cache中获取数据
public Bitmap getDataFromCache(int key) {
if (mCache.size() != 0) {
return mCache.get(key);
}
return null;
}
//3.将数据存储到Cache中
public void putDataToCache(int key, Bitmap bitmap) {
if (getDataFromCache(key) == null) {
mCache.put(key,bitmap);
}
}
...
复制代码
从代码中看首先对LruCache进行初始化,获取当前进程可用的内存,而后将内存缓存的容量制定为可用内存的1/8,同时对Bitmap对象进行大小的计算。接着构造出两个对外的方法,一个是根据Key从Cache中获取数据,一个是将数据存储到cache中。简单的3步也就完成了LruCache的使用。
磁盘缓存所使用的算法为DiskLruCache,它的使用比内存缓存要复杂一点,可是仍是离不开上面的3步,初始化,查找和添加。一样的,直接从代码中开始分析。
private final static int DISK_MAX_SIZE = 20 * 1024 * 1024;
private DiskLruCache mDiskLruCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化DiskLruCache。
File directory = getFile(this,"DiskCache");
if (!directory.exists()) {
directory.mkdirs();
}
try {
mDiskLruCache = DiskLruCache.open(directory, 1, 1, DISK_MAX_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
private File getFile(Context context,String dirName){
String filePath = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
? Objects.requireNonNull(context.getExternalCacheDir()).getPath() : context.getCacheDir().getPath();
return new File(filePath + File.pathSeparator + dirName);
}
复制代码
DiskLruCache的建立是DiskLruCache.open()来建立,其中会传入4个参数,第一个参数表示磁盘缓存所要存储的路径,通常来讲,若是外部设备存在,那么存储路径放置在 /storage/emulated/0/Android/data/package_name/cache 中;反之就放置在 /data/data/package_name/cache 这个目录下。存储路径能够根据本身的实际要求进行制定,值得注意的是,若是缓存路径选择SD卡上的缓存目录,即 /storage/emulated/0/Android/data/package_name/cache,那么当应用被卸载时,该目录也会被删除。
第二个参数表示应用的版本号,直接设置为1便可;第三个参数表示单个节点所对应的数据的个数,设置为1便可;第四个参数表示磁盘缓存的容量大小。
private void addDataToDisk(String url) {
//采用url的md5值做为key。
String key = hashKeyFromUrl(url);
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadDataFromNet(url, outputStream)) {
//提交至缓存
editor.commit();
} else {
//回退整个操做
editor.abort();
}
mDiskLruCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(url.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.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();
}
复制代码
DiskLruCache的添加主要是由DiskLruCache.Editor来完成,首先咱们会采用url的md5值来做为key,经过.Editor和key获取一个文件输出流,下载好图片经过这个文件输出流写入到文件系统中,最后经过editor.commit()的方法将文件提交才算真正将图片写入文件系统。
private Bitmap getDataFromDisk(String url) {
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(0);
return BitmapFactory.decodeStream(inputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
复制代码
DiskLruCache的添加是经过Editor来完成,而查找是由DiskLruCache.Snapshot来完成的。首先经过url获取到当前文件的key值,初始化Snapshot后获取一个文件输入流,最后经过该文件输入流来解析出当前缓存的文件。
上面已经分别描述了几种优化手段,最后再来总结一下。