Android 图片优化
png 图片压缩:
图片开源库:
优点:
- 多种图片格式的缓存,适用于更多的内容表现形式(如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会轻好多,上手快,使用简单,配置方便,而且从加载速度和性能方面不相上下。
1. 图片分辨率相关
分辨率适配问题。很多情况下图片所占的内存在整个App内存占用中会占大部分。我们知道可以通过将图片放到hdpi/xhdpi/xxhdpi等不同文件夹进行适配,通过xml android:background设置背景图片,或者通过BitmapFactory.decodeResource()方法,图片实际上默认情况下是会进行缩放的。在Java层实际调用的函数都是或者通过BitmapFactory里的decodeResourceStream函数:
1 public static Bitmap decodeResourceStream(Resources res, TypedValue value, 2 InputStream is, Rect pad, Options opts) { 3 if (opts == null) { 6 opts = new Options(); 7 } 8 if (opts.inDensity == 0 && value != null) { 10 final int density = value.density; 11 if (density == TypedValue.DENSITY_DEFAULT) 12 { 13 opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; 14 } 15 else if (density != TypedValue.DENSITY_NONE) 16 { 17 opts.inDensity = density; 18 } 19 } 20 if (opts.inTargetDensity == 0 && res != null) { 22 opts.inTargetDensity = res.getDisplayMetrics().densityDpi; 23 } 24 return decodeStream(is, pad, opts); 25 }
decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大,可以参考下腾讯Bugly的详细分析Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
关于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 |
e.g.
举个例子,对于一张1280×720的图片,如果放在xhdpi,那么xhdpi的设备拿到的大小还是1280×720而xxhpi的设备拿到的可能是1920×1080.
这两种情况在内存里的大小分别为:3.68M和8.29M,相差4.61M,在移动设备来说这几M的差距还是很大的。
尽管现在已经有比较先进的图片加载组件类似Glide,Facebook Freso, 或者老牌Universal-Image-Loader,但是有时就是需要手动拿到一个bitmap或者drawable,特别是在一些可能会频繁调用的场景(比如ListView的getView),怎样尽可能对bitmap进行复用呢?这里首先需要明确的是对同样的图片,要 尽可能复用,我们可以简单自己用WeakReference做一个bitmap缓存池,也可以用类似图片加载库写一个通用的bitmap缓存池,可以参考GlideBitmapPool的实现。
我们也来看看系统是怎么做的,对于类似在xml里面直接通过android:background或者android:src设置的背景图片,以ImageView为例,最终会调用Resource.java里的loadDrawable:
1 Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException 2 { 3 // Next, check preloaded drawables. These may contain unresolved theme 4 // attributes. 5 final ConstantState cs; 6 if (isColorDrawable) 7 { 8 cs = sPreloadedColorDrawables.get(key); 9 }else{ 10 cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); 11 } 12 13 Drawable dr; 14 if (cs != null) { 15 dr = cs.newDrawable(this); 16 } else if (isColorDrawable) { 17 dr = new ColorDrawable(value.data); 18 } else { 19 dr = loadDrawableForCookie(value, id, null); 20 } 21 22 ... 23 24 return dr; 25 }
可以看到实际上系统也是有一份全局的缓存,sPreloadedDrawables, 对于不同的drawable,如果图片时一样的,那么最终只会有一份bitmap(享元模式),存放于BitmapState中,获取drawable时,系统会从缓存中取出这个bitmap然后构造drawable。而通过BitmapFactory.decodeResource()则每次都会重新解码返回bitmap。所以其实我们可以通过context.getResources().getDrawable再从drawable里获取bitmap,从而复用bitmap.
然而这里也有一些坑,比如我们获取到的这份bitmap,假如我们执行了recycle之类的操作,但是假如在其他地方再使用它是那么就会有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”异常。
2. 图片压缩
BitmapFactory 在解码图片时,可以带一个Options,有一些比较有用的功能,比如:
-
inTargetDensity 表示要被画出来时的目标像素密度
-
inSampleSize 这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4
-
inJustDecodeBounds 字面意思就可以理解就是只解析图片的边界,有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片。
-
inPreferredConfig 默认会使用ARGB_8888,在这个模式下一个像素点将会占用4个byte,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte,一下可以省下50%内存。
-
inPurgeable和inInputShareable 这两个需要一起使用,BitmapFactory.java的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个bitmap,有点类似软引用,但是实际在5.0以后这两个属性已经被忽略,因为系统认为回收后再解码实际会反而可能导致性能问题
-
inBitmap 官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在4.4以前只有相同大小的图片内存区域可以复用,4.4以后只要原有的图片比将要解码的图片大既可以复用了。
2.1 质量压缩
(1)原理:保持像素的前提下改变图片的位深及透明度,(即:通过算法抠掉(同化)了图片中的一些某个些点附近相近的像素),达到降低质量压缩文件大小的目的。
注意:它其实只能实现对file的影响,对加载这个图片出来的bitmap内存是无法节省的,还是那么大。因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素(像素大小不会变)。
(2)使用场景:将图片压缩后将图片上传到服务器,或者保存到本地。根据实际需求来。
(3)源码示例
1 /** 2 * 质量压缩: 3 * 设置bitmap options属性,降低图片的质量,像素不会减少 4 * 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置 5 * 设置options 属性0-100,来实现压缩 6 * 7 * @param bmp 8 * @param file 9 */ 10 public static void qualityCompress(Bitmap bmp, File file) { 11 // 0-100 100为不压缩 12 int quality = 20; 13 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 14 // 把压缩后的数据存放到baos中 15 bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos); 16 try { 17 FileOutputStream fos = new FileOutputStream(file); 18 fos.write(baos.toByteArray()); 19 fos.flush(); 20 fos.close(); 21 } catch (Exception e) { 22 e.printStackTrace(); 23 } 24 }
2.2 尺寸压缩
(1)原理:通过减少单位尺寸的像素值,正真意义上的降低像素。1020*8880–
(2)使用场景:缓存缩略图的时候(头像处理)
(3)源码示例
1 /** 2 * 尺寸压缩:(通过缩放图片像素来减少图片占用内存大小) 3 * 4 * @param bmp 5 * @param file 6 */ 7 8 public static void sizeCompress(Bitmap bmp, File file) { 9 // 尺寸压缩倍数,值越大,图片尺寸越小 10 int ratio = 8; 11 // 压缩Bitmap到对应尺寸 12 Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888); 13 Canvas canvas = new Canvas(result); 14 Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio); 15 canvas.drawBitmap(bmp, null, rect, null); 16 17 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 18 // 把压缩后的数据存放到baos中 19 result.compress(Bitmap.CompressFormat.JPEG, 100, baos); 20 try { 21 FileOutputStream fos = new FileOutputStream(file); 22 fos.write(baos.toByteArray()); 23 fos.flush(); 24 fos.close(); 25 } catch (Exception e) { 26 e.printStackTrace(); 27 } 28 }
2.3 采样率压缩
(1)原理:设置图片的采样率,降低图片像素
(2) 好处:是不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜。
(3)问题:因为采样率是整数,所以不能很好的保证图片的质量。如我们需要的是在2和3采样率之间,用2的话图片就大了一点,但是用3的话图片质量就会有很明显的下降,这样也无法完全满足我的需要。
(4)源码示例
1 /** 2 * 采样率压缩(设置图片的采样率,降低图片像素) 3 * 4 * @param filePath 5 * @param file 6 */ 7 public static void samplingRateCompress(String filePath, File file) { 8 // 数值越高,图片像素越低 9 int inSampleSize = 8; 10 BitmapFactory.Options options = new BitmapFactory.Options(); 11 options.inJustDecodeBounds = false; 12 // options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。 13 //采样率 14 options.inSampleSize = inSampleSize; 15 Bitmap bitmap = BitmapFactory.decodeFile(filePath, options); 16 17 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 18 // 把压缩后的数据存放到baos中 19 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); 20 try { 21 if (file.exists()) { 22 file.delete(); 23 } else { 24 file.createNewFile(); 25 } 26 FileOutputStream fos = new FileOutputStream(file); 27 fos.write(baos.toByteArray()); 28 fos.flush(); 29 fos.close(); 30 } catch (Exception e) { 31 e.printStackTrace(); 32 } 33 }
3. 缓存池大小
现在很多图片加载组件都不仅仅是使用软引用或者弱引用了,实际上类似Glide 默认使用的事LruCache,因为软引用 弱引用都比较难以控制,使用LruCache可以实现比较精细的控制,而默认缓存池设置太大了会导致浪费内存,设置小了又会导致图片经常被回收,所以需要根据每个App的情况,以及设备的分辨率,内存计算出一个比较合理的初始值,可以参考Glide的做法。
4 想办法减少 Bitmap 内存占用:
4.1 Jpg 和 Png
jpg 是一种有损压缩的图片存储格式,而 png 则是 无损压缩的图片存储格式,显而易见,jpg 会比 png 小.
Bitmap 在内存当中占用的大小其实取决于:
-
色彩格式,前面我们已经提到,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
-
原始文件存放的资源目录(是 hdpi 还是 xxhdpi 可不能傻傻分不清楚哈)
-
目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)
如果仅仅是为了 Bitmap 读到内存中的大小而考虑的话,jpg 也好 png 也好,没有什么实质的差别;二者的差别主要体现在:
-
alpha 你是否真的需要?如果需要 alpha 通道,那么没有别的选择,用 png。
-
你的图色值丰富还是单调?就像刚才提到的,如果色值丰富,那么用jpg,如果作为按钮的背景,请用 png。
-
对安装包大小的要求是否非常严格?如果你的 app 资源很少,安装包大小问题不是很凸显,看情况选择 jpg 或者 png(不过,我想现在对资源文件没有苛求的应用会很少吧。。)
-
目标用户的 cpu 是否强劲?jpg 的图像压缩算法比 png 耗时。这方面还是要酌情选择,前几年做了一段时间 Cocos2dx,由于资源非常多,项目组要求统一使用 png,可能就是出于这方面的考虑。
4.2 使用 inSampleSize (采样率压缩)
这个方法主要用在图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出地目标可能相对较小,对图片分辨率、大小要求不是非常的严格。
既然图片最终是要被模糊的,也看不太情况,还不如直接用一张采样后的图片,如果采样率为 2,那么读出来的图片只有原始图片的 1/4 大小:
1 BitmapFactory.Options options = new Options(); 2 options.inSampleSize = 2; 3 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
4.3 使用矩阵
大图小用用采样,小图大用用矩阵。
还是用前面模糊图片的例子,我们不是采样了么?内存是小了,可是图的尺寸也小了啊,我要用 Canvas 绘制这张图可怎么办?当然是用矩阵了:
方式一:
1 Matrix matrix = new Matrix(); 2 matrix.preScale(2, 2, 0f, 0f); 3 //如果使用直接替换矩阵的话,在Nexus6 5.1.1上必须关闭硬件加速 4 canvas.concat(matrix); 5 canvas.drawBitmap(bitmap, 0,0, paint);
需要注意的是,在使用搭载 5.1.1 原生系统的 Nexus6 进行测试时发现,如果使用 Canvas 的 setMatrix 方法,可能会导致与矩阵相关的元素的绘制存在问题,本例当中如果使用 setMatrix 方法,bitmap 将不会出现在屏幕上。因此请尽量使用 canvas 的 scale、rotate 这样的方法,或者使用 concat 方法。
方式二:
1 Matrix matrix = new Matrix(); 2 matrix.preScale(2, 2, 0, 0); 3 canvas.drawBitmap(bitmap, matrix, paint);
这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小。
如果我要把图片放到 ImageView 当中呢?一样可以,请看:
1 Matrix matrix = new Matrix(); 2 matrix.postScale(2, 2, 0, 0); 3 imageView.setImageMatrix(matrix); 4 imageView.setScaleType(ScaleType.MATRIX); 5 imageView.setImageBitmap(bitmap);
4.4 合理选择Bitmap的像素格式
其实前面我们已经多次提到这个问题。
ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte。我们先看下有多少种格式可选:
格式 | 描述 |
ALPHA_8 | 只有一个alpha通道 |
ARGB_4444 | 这个从API 13开始不建议使用,因为质量太差 |
ARGB_8888 | ARGB四个通道,每个通道8bit |
RGB_565 | 每个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit |
这几个当中,
ALPHA8 没必要用,因为我们随便用个颜色就可以搞定的。
ARGB4444 虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃,失宠了。。『又要占省内存,又要看着爽,臣妾做不到啊T T』。
ARGB8888 是最常用的,大家应该最熟悉了。
RGB565 看到这个,我就看到了资源优化配置无处不在,这个绿色。。(不行了,突然好邪恶XD),其实如果不需要 alpha 通道,特别是资源本身为 jpg 格式的情况下,用这个格式比较理想。