Bitmap性能优化

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言

Bitmap在Android中的使用非常的广泛,几乎每个页面都有使用它,特别是在一些ListView/GridView/RecyclerVIew等需要大量用到图片的场景,合理的使用Bitmap就显得尤为重要了,稍有不慎就可能会导致OOM

Bitmap内存占用计算方式

重要:将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,可能一张图片只有100k你觉得它并不大,但是读取到内存当中是按照像素点来算的

比如一张图片是1500 * 1000像素,使用的ARGB_8888颜色类型,即(8+8+8+8)32位,那么每个像素点就会占用4个字节,总内存就是1500 * 1000 * 4字节,也就是5.7MB

优化技巧

  1. 设置合适的Bitmap.Config
public static final Bitmap.Config  ALPHA_8
public static final Bitmap.Config  ARGB_4444 
public static final Bitmap.Config  ARGB_8888
public static final Bitmap.Config  RGB_565

根据bitmap的计算方式我们知道了,bitmap的配置参数也是影响bitmap占用内存大小的一个因素。比如如果你不考虑图片透明度的话,ARGB_8888RGB_565他们的表现效果在用户看来是一样的,但是RGB_565(5+6+5 = 16位 = 2字节)的内存占用只有ARGB_8888(8+8+8+8 = 32位 = 4字节)的一半。所以在不影响用户体验的情况下,应该采用内存占用更少的方案。在某些低性能设备上,甚至可以采用牺牲部分视觉效果换取更流畅的用户体验的降级方案,如用ARGB_4444代替ARGB_8888

  1. 设置合适的inSampleSize

有时候我们页面上所需要展示的图片宽高(单位像素)远远小于图片的原始宽高,如果把图片直接展示不做任何处理的话,拿我们上面那个例子来说,一张图片就是5.7MB,显然我们是无法接受的。那么我们就可以通过设置inSampleSize属性来压缩我们的图片(对用户来说视觉上是没有任何影响的)

inSampleSize可以理解为采样率,它的默认值和最小值为1(当小于1时,解码器将该值当做1来处理),且在大于1时,该值只能为2的幂(当不为2的幂时,解码器会取与该值最接近的2的幂)

例如,当inSampleSize为2时,一个2000 * 1000的图片,将被缩小为1000 * 500,相应地,它的像素数和内存占用都被缩小为了原来的1/4

为了设置合适的inSampleSize,我们就需要获取到原始Bitmap的宽高和实际所需宽高,实际所需的宽高我们根据实际业务场景很容易测量,那原始BItmap的宽高如何进行测量呢?这里有一个比较关键的地方,因为我们并不想把这个Bitmap加载到内存中后再去测量,幸运的是,Android给我们提供了相关的解决方案

BitmapFactory这个类提供了多个解析方法用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法

Android为每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true,就可以让解析方法禁止为Bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。但是BitmapFactory.OptionsoutWidthoutHeightoutMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:

public static Bitmap decodeBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
    // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 计算inSampleSize值
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 还原inJustDecodeBounds为false
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

 public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        final int width = options.outWidth;
        final int height = options.outHeight;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            //计算图片高度和我们需要高度的最接近比例值
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            //宽度比例值
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            //取比例值中的较大值作为inSampleSize
            inSampleSize = heightRatio > widthRatio ? heightRatio : widthRatio;
        }

        return inSampleSize;
    }

不过要注意的是,上面这个inSampleSize的算法也不是万能的,比如我们平时拍的照片都是长图,这种算法用在浏览相册图片的缩略图的时候就会导致压缩太过严重,导致图片不是很清楚。所以关于inSampleSize的设置算法,还是需要根据自己的实际业务场景而定,设计一个合适的inSampleSize

  1. 使用LRU缓存技术

为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生

LruCache 这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除

下面是一个使用 LruCache 来缓存图片的例子:

private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
  // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
  // LruCache通过构造函数传入缓存值,以KB为单位。
  int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
  // 使用最大可用内存值的1/8作为缓存的大小。
  int cacheSize = maxMemory / 8;
  mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
      // 重写此方法来衡量每张图片的大小,默认返回图片数量。
      return bitmap.getByteCount() / 1024;
    }
  };
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
  if (getBitmapFromMemCache(key) == null) {
    mMemoryCache.put(key, bitmap);
  }
}
public Bitmap getBitmapFromMemCache(String key) {
  return mMemoryCache.get(key);
}

在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(8004804)。因此,这个缓存大小可以存储2.5页的图片

当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片

public void loadBitmap(int resId, ImageView imageView) {
  final String imageKey = String.valueOf(resId);
  final Bitmap bitmap = getBitmapFromMemCache(imageKey);
  if (bitmap != null) {
    imageView.setImageBitmap(bitmap);
  } else {
    imageView.setImageResource(R.drawable.image_placeholder);
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
  }
}
  1. 使用inBitmap复用Bitmap对象

利用options.inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率。使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张BitmapHeap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小

使用inBitmap前.png
使用inBitmap后.png

使用inBitmap需要注意几个限制条件:

  • 在SDK 11 -> 18之间,重用的Bitmap大小必须是一致的。例如给inBitmap赋值的图片大小为100 * 100,那么新申请的Bitmap必须也为100 * 100才能够被重用。从SDK 19开始,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmap大小

  • 新申请的Bitmap与旧的Bitmap必须有相同的解码格式。例如大家都是ARGB_8888的,如果前面的BitmapARGB_8888,那么就不能支持ARGB_4444RGB_565格式的Bitmap了。我们可以创建一个包含多种典型可重用Bitmap的对象池,这样后续的Bitmap创建都能够找到合适的“模板”去进行重用

  1. 使用.9图

现在手机的分辨率越来越高,图片资源在被加载后所占用的内存也越来越大,所以要尽量避免使用大的PNG图,在产品设计的时候就要尽量避免用一张大图来进行展示,尽量多用NinePatch资源

NinePatch指的是一种拉伸后不会变形的特殊png图,NinePatch的拉伸区域可以自己定义。这种图的优点是体积小,拉伸不变形,可以适配多机型

Android SDK中有自带NinePatch资源制作工具,Android-Studio中在普通png图片点击右键可以将其转换为NinePatch资源,使用起来非常方便

  1. 主动释放资源
  • 主动释放Bitmap资源

当你确定这个Bitmap资源不会再被使用的时候(当然这个Bitmap不释放可能会让程序下一次启动或者resume快一些,但是其占用的内存资源太大,可能导致程序在后台的时候被杀掉,反而得不偿失),我们建议手动调用recycle()方法,释放内存

   private static void rceycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
            bitmap = null;
        }
   }
  • 主动释放ImageView的图片资源

由于我们在实际开发中,很多情况是在xml布局文件中设置ImageViewsrc或者在代码中调用ImageView.setImageResource/setImageURI/setImageDrawable等方法设置图像,下面代码可以回收这个ImageView所对应的资源

  private static void recycleImageViewBitMap(ImageView imageView) {
        if (imageView != null) {
            BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
            rceycleBitmapDrawable(bd);
        }
    }

    private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
        if (bitmapDrawable != null) {
            Bitmap bitmap = bitmapDrawable.getBitmap();
            rceycleBitmap(bitmap);
        }
        bitmapDrawable = null;
    }
  • 主动释放ImageView的背景资源

如果ImageView是有Background,那么下面的代码可以进行背景资源的释放:

public static void recycleBackgroundBitMap(ImageView view) {
    if (view != null) {
        BitmapDrawable bd = (BitmapDrawable) view.getBackground();
        rceycleBitmapDrawable(bd);
    }
}
private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
    if (bitmapDrawable != null) {
        Bitmap bitmap = bitmapDrawable.getBitmap();
        rceycleBitmap(bitmap);
    }
    bitmapDrawable = null;
}

总结

本文主要讲了一些Bitmap的优化技巧,但是在实际生产环境中,建议使用Glide等图片框架,因为这些框架内部往往已经帮我们做了很多的优化,例如缓存、Bitmap复用、压缩等等操作,但是明白其中的原理对我们开发者也是很有意义,往往能帮我们解决一些框架没有覆盖到的疑难问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容

  • 7.1 压缩图片 一、基础知识 1、图片的格式 jpg:最常见的图片格式。色彩还原度比较好,可以支持适当压缩后保持...
    AndroidMaster阅读 2,516评论 0 13
  • 一直以来Bitmap都是开发中很棘手的问题,这个问题就是传说中的OOM(java.lang.OutofMemory...
    M悇芐冋忆阅读 4,787评论 0 11
  • 一年一度为时三天的高考终于结束。硝烟过后周末似乎异常平静。看着那些严肃的考生和忧虑的家长,忍住不情怀泛滥想回忆自己...
    王耐思阅读 304评论 1 4
  • 今天是4月23日,世界读书日。看见各大读书app上推了各种优惠活动,不禁想起前段时间经常在公众号上看见的各种...
    徐子诺_1949阅读 191评论 0 2
  • 记得小时候夏天的夜晚,我们姊妹几个围着爷爷要他讲织女牛郎的故事,那时候天上的星星总是很亮很多,也会时常仰望星空,黄...
    米皮_87d6阅读 236评论 0 0