Android 图片优化

png 图片压缩:

Android 美工工具,以及图片压缩

Android icon 分辨率

图片开源库:

Glide

优点:

  • 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
  • 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
  • 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
  • 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)

缺点:

  • 没有文件缓存 
  • java heap比Fresco高

Picasso

和Square的网络库一起能发挥最大作用,因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现。使用4.0+系统上的HTTP缓存来代替磁盘缓存.
Picasso 底层是使用OkHttp去下载图片,所以Picasso底层网络协议为Http.

不建议使用了;

Fresco

优点:

  • 最大的优势在于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. 图片压缩

Android 大位图加载

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%内存。

  • inPurgeableinInputShareable 这两个需要一起使用,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. 缓存池大小

Android LruCache(Picasso内存缓存)

 现在很多图片加载组件都不仅仅是使用软引用或者弱引用了,实际上类似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 格式的情况下,用这个格式比较理想。

 

posted @ 2018-06-15 17:40  晕菜一员  阅读(1590)  评论(0编辑  收藏  举报