相关文章推荐
跑龙套的橡皮擦  ·  c# httpclient disable ...·  1 年前    · 
微醺的凉茶  ·  Oops!!! - 简书·  1 年前    · 
  • 多种图片格式的缓存,适用于更多的内容表现形式(如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的内存)
  • 适用于需要高性能加载大量图片的场景
  • 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();
     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);
    

    decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大,可以参考下腾讯Bugly的详细分析Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?

    关于Density、分辨率、-hdpi等res目录之间的关系:

    DensityDpi Density 160dpi 320*533 240dpi 480*800 320dpi 720*1280 xhdpi 480dpi 1080*1920 xxhdpi 560dpi 1440*2560 xxxhdpi
    举个例子,对于一张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 
     3     // Next, check preloaded drawables. These may contain unresolved theme
     4     // attributes.
     5     final ConstantState cs;
     6     if (isColorDrawable)
     8         cs = sPreloadedColorDrawables.get(key);
     9     }else{
    10         cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
    11     }
    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     }
    22     ...
    24     return dr;
    

    可以看到实际上系统也是有一份全局的缓存,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,来实现压缩
     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         }
    

    2.2 尺寸压缩

    (1)原理:通过减少单位尺寸的像素值,正真意义上的降低像素。1020*8880–

    (2)使用场景:缓存缩略图的时候(头像处理)

    (3)源码示例

     1 /**
     2      * 尺寸压缩:(通过缩放图片像素来减少图片占用内存大小)
     4      * @param bmp
     5      * @param file
     6      */
     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);
    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         }
    

    2.3 采样率压缩

    (1)原理:设置图片的采样率,降低图片像素

    (2) 好处:是不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜。

    (3)问题:因为采样率是整数,所以不能很好的保证图片的质量。如我们需要的是在2和3采样率之间,用2的话图片就大了一点,但是用3的话图片质量就会有很明显的下降,这样也无法完全满足我的需要。

    (4)源码示例

     1 /**
     2      * 采样率压缩(设置图片的采样率,降低图片像素)
     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);
    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         }
    

    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 格式的情况下,用这个格式比较理想。