android性能优化之图片Bitmap优化(含源码)

android应用中,一般都会大量用到图片加载,使用不当会引起OOM,在glide出现前,我们可能会经常遇到图片OOM的问题,那么我们假设没有glide,如何来考虑优化呢?这样考虑有助于我们深入理解图片优化,以及性能优化。
我们分三个步骤来详解此问题

  1. Bitmap基础
  2. 缩放与缓存优化(glide帮助我们做了这一步的优化,但此处我们自己来简单实现)
  3. 长图加载优化

Bitmap基础

  • 内存大小如何计算
    • ARGB_8888: ARGB各占8位,内存为 Width * Height * 4
    • RGB_565: R5位,G6位,B5位,内存为Width * Height * 2
  • 如何获取 Bitmap 所占内存
    • getByteCount(): 返回可用于存储此位图像素的 最小字节数
  • BitmapFactory.Options控制解码图片参数
    • inDensity: 便是这个bitmap的像素密度,根据drawable目录
      • drawable-ldpi:120
      • drawable-mdpi:160
      • drawable-hdpi:240
      • drawable-xhdpi:320
      • drawable-xxhdpi:480
    • inTargetDensity: 表示要被画出来时的目标(屏幕)的像素密度,代码中获取方式 getResources().getDIsplayMetrics().densityDpi
  • Bitmap内存压缩:我们在使用图片的时候,选择jpg、png或者webp,对内存都有影响
  • BitmapFactory.options
    • inJustDecodeBounds: 为true时,decoder将返回null,但是为解析出 outxxx字段
    • inPreferredConfig: 设置图片解码后的像素格式,如ARGB_8888/RGB_565
    • inSampleSize: 设置图片解码缩放比,如值为2,则加载图片的宽高是原来的1/2,整个图片所占内存的大小是原图的 1/4
  • 内存缓存:LRUCache,最近最少使用算法,里面使用的是LinkedHashMap来存储的

缩放优化

  • 如果一个ImageView在屏幕上显示的宽度和高度只有100像素,而图片本身宽度和高度有1000像素,那么我们把整个图片完全转成Bitmap加载进来,是不是会造成内存上很大的浪费?答案是肯定的,那么我们解决思路就很明确了,将图片缩放到需要显示的宽度和高度,然后再加载进来,而 BitmapFactory 也提供给了我们方法这么做
  • 解决方案
    1. 计算缩放比例
    2. 根据缩放比例,获取缩放后图片,然后返回并加载
    //1. 计算缩放比例
    //w表示图片实际宽度,h表示图片实际高度
    //maxW表示图片在屏幕上可显示的最大宽度,maxH表示图片在屏幕上可显示的最大高度
	private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
        int inSampleSize = 1;
        if (w > maxW && h > maxH) {
            inSampleSize = 2;
            while (w / inSampleSize > maxW && h / inSampleSize > maxH) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
    //2. 根据缩放比例,获取缩放后图片
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha) {
        Resources resources = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 设置为true后,再去解析,就只解析 out 参数
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources, id, options);
        int w = options.outWidth;
        int h = options.outHeight;
        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);
        if (!hasAlpha) {
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false;
        //根据缩放比例,获取缩放后图片
        return BitmapFactory.decodeResource(resources, id, options);
    }

缓存优化

在做过缩放优化后,如果在列表的每一项中都有图片的时候,上下滑动列表,为了显示图片,程序会频繁解析图片、快速申请内存、快速释放内存,频繁解析会造成cpu资源的消耗,快速申请和释放会造成内存抖动,从而频繁gc,而gc时会STW(stop the world),从而引起卡顿。

为了解决上述问题,我们采用三级缓存来缓存Bitmap资源,从而优化内存使用。

  1. LruCache:内存级别,存储后,需要显示时直接取出来,直接显示到ImageView上就行
  2. 缓存池缓存:内存级别,与Lru不同的是LruCache可以直接复用Bitmap资源,而缓存池只能复用内存空间,简单地说,就是缓存池中Bitmap是一个空对象,使用时需要重新为其赋值,复用内存空间,就不会频繁去申请和释放
  3. 磁盘缓存:磁盘级别
LruCache

LruCache,使用最近最少使用算法,加载过的图片优先放到LruCache中,需要加载图片时先从LruCache中尝试去取,可以取到时便直接用,不需要重新解析,不需要重新创建Bitmap

  • 初始化
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
//应用可用的内存总大小,单位是Mb,*1024*1024是转化成了字节
int memoryClass = am.getMemoryClass();
	LruCache<String, Bitmap> lruCache =   new LruCache<String, Bitmap>(memoryClass * 1024 * 1024 / 8) {
            // 返回的一张图片大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                    return value.getAllocationByteCount();
                }
                return value.getByteCount();
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                oldValue.recycle();
            }
        };
  • 对外提供存入和取出方法
//把bitmap 放入内存
public void putBitmap2Memory(String key, Bitmap bitmap) {
        lruCache.put(key, bitmap);
}
//从内存中取出bitmap
public Bitmap getBitmapFromMemory(String key) {
        return lruCache.get(key);
}
缓存池缓存

当bitmap从LruCache中移除时,gc可能并未来得及进行清理,这个时候我们通过弱引用把从LruCache中移除的bitmap放到缓存池中,当从LruCache中取不到对应的Bitmap时,就从缓存池中取出一个bitmap,从而达到复用内存空间的目的。

  • 初始化
//弱引用,是为了不影响gc回收内存空间,此处创建一个线程安全的Set作为池
Set<WeakReference<Bitmap>> reusablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
  • 当bitmap从LruCache中移除时,添加到缓存池
lruCache = new LruCache<String, Bitmap>(){
	@Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if (oldValue.isMutable()) {
                    // 3.0 bitmap FW层缓存到 native
                    // <8.0  bitmap FW层缓存到 java
                    // 8.0 bitmap FW层缓存到 native
                    reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
                }
                oldValue.recycle();
            }
}
  • 对外提供复用方法,根据图片宽高
// 3.0前不能复用
// 3.0-4.4,宽高一样,缩放比=1,才能复用
// 4.4之后,只要需要的空间大小够用就可以复用
public Bitmap getReusable(int w, int h, int inSampleSize) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return null;
        }
        Bitmap reusable = null;
        Iterator<WeakReference<Bitmap>> iterator = reusablePool.iterator();
        while (iterator.hasNext()) {
            Bitmap bitmap = iterator.next().get();
            if (bitmap != null) {
                if (checkInBitmap(bitmap, w, h, inSampleSize)) {
                    reusable = bitmap;
                    iterator.remove();
                    break;
                }
            } else {
                iterator.remove();
            }
        }
        return reusable;
    }
磁盘缓存

使用DiskLruCache进行磁盘缓存,这样一个图片资源解析成Bitmap后,使用时,就不需要再解析,直接从磁盘中取出放到内存中就可以了

  • 初始化
DiskLruCache diskLruCache =  DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);
  • 对外提供写入和读取方法
//放入磁盘缓存
public void putBitmap2Disk(String key, Bitmap bitmap) {
        DiskLruCache.Snapshot snapshot = null;
        OutputStream os = null;
        snapshot = diskLruCache.get(key);
            if (snapshot == null) {
                DiskLruCache.Editor edit = diskLruCache.edit(key);
                if (edit != null) {
                    os = edit.newOutputStream(0);
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);
                    edit.commit();
                }
            }
    }
//从磁盘缓存获取bitmap
public Bitmap getBitmapFromDisk(String key, Bitmap reusable) {
        DiskLruCache.Snapshot snapshot = null;
        Bitmap bitmap = null;
        snapshot = diskLruCache.get(key);
            if (snapshot == null) {
                return null;
            }
            InputStream is = snapshot.getInputStream(0);

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inMutable = true;
            options.inBitmap = reusable;
            bitmap = BitmapFactory.decodeStream(is, null, options);
            if (bitmap != null) {
                lruCache.put(key, bitmap);
            }
        return bitmap;
    }

长图加载

如果一个长图有几十兆甚至是上百兆,如果我们直接使用BitmapFactory去解析,将会引起OOM,如何去解决呢?

我们可以使用系统提供的BigmapRegionDecoder来加载指定区域,将宽度缩放到屏幕宽度,先加载屏幕高度的区域,滑动时候再根据滑动区域加载对应的区域,可以大大节省内存占用。

InputStream is = getAssets().open("big.png");
//创建一个BigmapRegionDecoder对象,
BigmapRegionDecoder decoder = BigmapRegionDecoder.newInstance(is,false);
//获取Rect指定区域的图片
Bitmap  bitmap = decoder.decodeRegion(Rect,null);

上述优化案例的github源码地址

图片压缩

通过哈夫曼编码对图片进行压缩,可以在不影响图片清晰度的情况下,对图片进行大幅压缩,详情请点击Android图片压缩——Luban鲁班压缩

实际应用

实际使用中,我们可以直接使用第三方库来解决,基本原理类似,缓存用glide,大图用BigImageViewer

  1. glide的github地址
  2. BigImageViewer
  3. Luban

导航

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值