为什么要进行图片优化?
- 1、减少内存消耗 (容易造成OOM)
- 2、App流畅度好
- 3、减少流量损耗
- 4、减少APK包大小
- 5、提高用户体验
什么是OOM?
简单的讲就是内存耗尽了。Android系统的进程(APP级别)有最大内存限制,超过这个限制系统就会抛出OOM错误。
图片OOM问题产生原因:
一个页面一次加载过多图片
加载大图片没有进行压缩(尺寸、质量)
Android列表加载大量bitmap没有使用缓存
如何进行图片优化?——存储优化、加载优化
设置正确的图片格式
目前移动端Android平台原生支持的图片格式主要有:JPEG、PNG、GIF、BMP、和WebP(自从Android 4.0开始支持),但是在Android应用开发中能够使用的编解码格式只有三种:JPEG、PNG、WebP,图片格式可以通过查看Bitmap类的CompressFormat枚举值来确定。
public static enum CompressFormat {
JPEG、
PNG、
WebP;
private CompressFormat() {
}
}
如果要在应用层使用GIF格式图片,那么需要自己引用第三方函数库进行支持。
- png:无损压缩图片格式,支持完整的Alpha通道,Android切图素材多采用此格式
- jpeg:有损压缩图片格式,不支持透明和多帧动画,适用于照片等色彩丰富的大图压缩,不适合logo
- GIF:是一种古老的图片格式,它诞生于1987年,它的特点是支持多帧动画。(需要采用第三方框架加载)
- webp:是一种同时提供了有损压缩和无损压缩的图片格式,派生自视频编码格式VP8,从谷歌官网来看,无损webp平均比png小26%,有损的webp平均比jpeg小25%~34%,无损webp支持Alpha通道,有损webp在一定的条件下同样支持,有损webp在Android4.0(API 14)之后支持,无损和透明在Android4.3(API18)之后支持
采用webp能够在保持图片清晰度的情况下,可以有效减小图片所占有的磁盘空间大小。如果你的APP最低支持到Android4.0,那么可以直接使用系统提供的能力来支持WebP,如果是4.0以下的系统,也可以通过在APP中集成第三方函数库例如webp-android-backport来实现对WebP的支持。
使用.9图。.9.png本质上仍然是PNG格式图片,它是针对Android平台的一种特殊PNG格式图片,可以在图片指定位置拉伸或者填充内容。.9图的优点是体积小,拉伸不变形,能够很好地适配Android各种机型
图片分辨率适配
同一张图片,放置在不同的目录下,会生成大小不同的bitmap。
APP在查找图片资源的时候遵循先高后低的原则,假设设备的分辨率是xxhdpi,那么查找顺序如下
- 先去drawable-xxhdpi文件夹查找,如果有这张图片就使用,这个时候图片不会缩放
- 如果没有找到,则去更高密度的文件夹下找,例如drawable-xxxhdpi,密度依次递增,如果找到了,图片将会缩小,因为系统认为这些图片都是给高分辨率设备使用的
- 所有高密度文件夹都没有的话,就会去drawable-nodpi文件夹去找,如果有也不会缩放
- 还是没有的话,就会去更低密度的文件夹下面找,xhdpi,hdpi等,密度依次递减,如果找到了,图片将会放大,因为系统认为这个图片是给低分辨率设备使用的
mipmap文件夹:谷歌建议是将启动图片放置在mipmap中,其他的图片仍然放在drawable文件夹中。
关于Density、分辨率、-hdpi等res目录之间的关系:
DensityDpi | 分辨率 | 屏幕密度 | Density |
160dpi | 320*533 | mdpi | 1 |
240dpi | 480*800 | hdpi | 1.5 |
320dpi | 720*1280 | xhdpi | 2 |
480dpi | 1080*1920 | xxhdpi | 3 |
560dpi | 1440*2560 | xxxhdpi | 3 |
xxhdpi密度设备在xhdpi文件夹下找到匹配的资源(xxhdpi及xxxhpi中都没有找到),系统会认为你这张图是专门为低密度的设备所设计的,
如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。
举例:对于一张1280×720的图片,如果放在xhdpi,那么xhdpi的设备拿到的大小是1280×720,而xxhpi的设备拿到的可能是1920×1080.
这两种情况在内存里的大小分别为:3.68M和8.29M,在移动设备来说这几M的差距还是很大的。(可以用profiler监控到内存大小)
质量压缩(compress)
压缩bitmap到目标大小
原理:保持像素的前提下改变图片的位深及透明度,只影响file的大小,加载这个图片出来的bitmap内存是无法节省的
使用场景:将图片压缩后将图片上传到服务器,或者保存到本地
/**
* 质量压缩:
* 设置bitmap options属性,降低图片的质量,像素不会减少
* 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
* 设置options 属性0-100,来实现压缩
*
* 0-100 100为不压缩
*/
int quality = 20;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
等比(尺寸)压缩(createBitmap)
原理:通过减少单位尺寸的像素值,真正意义上的降低像素
使用场景:缓存缩略图的时候(头像处理)
其实ThumbnailUtils.extractThumbnail()获取缩略图就是这种压缩方式;
float sourceWidth = bitmap.getWidth();
float sourceHeight = bitmap.getHeight();
float scaleWidth = targetWidth / sourceWidth;
float scaleHeight = targetHeight / sourceHeight;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight); //长和宽放大缩小的比例
Bitmap bm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
采样率压缩(option.inSimpleSize)
原理:设置图片的采样率,降低图片像素
好处:是不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜
不足:因为采样率是整数,所以不能很好的保证图片的质量。
final BitmapFactory.Options options = new BitmapFactory.Options();
//预加载,只加载宽高不加载bitmap,不占内存
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imageFile, options); //加载图片信息
int sourceWidth = options.outWidth;
int sourceHeight = options.outHeight;
options.inJustDecodeBounds = false;
//计算inSampleSize
int inSampleSize = 1;
//先根据宽度进行缩小
while (sourceWidth / inSampleSize > targetWidth) {
inSampleSize++;
}
//然后根据高度进行缩小
while (sourceHeight / inSampleSize > targetHeight) {
inSampleSize++;
}
if (inSampleSize <= 0) {
inSampleSize = 1;
}
options.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeFile(imageFile, options);//加载真正bitmap
位图格式压缩(RGB_565)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565; //将格式设置成RGB_565
bm = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/DCIM/Camera/test.jpg", options);
注意:由于ARGB_4444的画质惨不忍睹,一般假如对图片没有透明度要求的话,可以改成RGB_565,相比ARGB_8888将节省一半的内存开销
使用矩阵
大图小用用采样,小图大用用矩阵。
我们进行采样,内存是小了,可是图的尺寸也小了,要用 Canvas 绘制这张图可怎么办?当然是用矩阵了:
方式一:
Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0f, 0f);
//如果使用直接替换矩阵的话,在Nexus6 5.1.1上必须关闭硬件加速
canvas.concat(matrix);
canvas.drawBitmap(bitmap, 0,0, paint);
需要注意的是,在使用搭载 5.1.1 原生系统的 Nexus6 进行测试时发现,如果使用 Canvas 的 setMatrix 方法,可能会导致与矩阵相关的元素的绘制存在问题,本例当中如果使用 setMatrix 方法,bitmap 将不会出现在屏幕上。因此请尽量使用 canvas 的 scale、rotate 这样的方法,或者使用 concat 方法。
方式二:
Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);
这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小。
如果我要把图片放到 ImageView 当中,一样可以:
Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);
合理选择Bitmap的像素格式
其实前面我们已经多次提到这个问题。
ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte。我们先看下有多少种格式可选:
格式 | 描述 |
ALPHA_8 | 只有一个alpha通道,使用场景特殊,比如设置遮盖效果等。 |
ARGB_4444 | 这种方案每个通道都是4位,每个像素占用2个字节,图片的失真比较严重。这个从API 13开始不建议使用,因为质量太差 |
ARGB_8888 | ARGB四个通道,每个通道8bit,每个像素点需要4字节的内存空间来存储数据。该方案图片质量是最高的,但是占用的内存也是最大的 |
RGB_565 | RGB通道值红色占5bit,绿色占6bit,蓝色占5bit,但是没有存储A通道值,所以不支持透明度。每个像素点占用2字节,是ARGB_8888方案的一半。 |
比较分析:一般我们在ARGB_8888方式和RGB_565方式中进行选取:不需要设置透明度时,比如拍摄的照片等,RGB_565是个节省内存空间的不错的选择;既要设置透明度,对图片质量要求又高,就用ARGB_8888。
Bitmap内存复用与及时释放
何为内存复用,通俗来说就是我已经创建了一个Bitmap对象了,那么我接着还想用一个bitmap对象,那么就可以复用上一个Bitmap对象,不过这样做有两个缺点,第一就是会将之前的图片覆盖掉,第二就是后边加载的图片必须小于或者等于之前的图片大小,否则就不行。
var bitmapOptions1 = BitmapFactory.Options()
bitmapOptions1.inBitmap = bitmap//绑定一个已经加载的Bitmap对象
var bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.timg, bitmapOptions1)//加载新的Bitmap对象
this.imgMain1.setImageBitmap(bitmap1)//使用Bitmap
bitmap使用完应该及时释放资源,避免内存占用。
图片的缓存提升加载效率
图片的缓存框架有很多,常见的有四种
- Android-Universal-Image-Loader
- Picasso
- Glide
- Fresco
图片开源库:
优点:
- 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
- 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
- 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
- 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)
缺点:
- 没有文件缓存
- java heap比Fresco高
和Square的网络库一起能发挥最大作用,因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现。使用4.0+系统上的HTTP缓存来代替磁盘缓存.
Picasso 底层是使用OkHttp去下载图片,所以Picasso底层网络协议为Http.
不建议使用了;
优点:
- 最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区)
- 大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存)
- 适用于需要高性能加载大量图片的场景
缺点:
- 包较大(2~3M)
- 用法复杂
- 底层涉及c++领域,阅读源码深入学习难度大
Fresco虽然很强大,但是包很大,依赖很多,使用复杂,而且还要在布局使用SimpleDraweeView控件加载图片。相对而言Glide会轻好多,上手快,使用简单,配置方便,而且从加载速度和性能方面不相上下。