Android 图片性能优化:Bitmap

Android 图片性能优化:Bitmap,第1张

一、引入

        图片作为内存消耗大户,一直是开发人员尝试优化的重点对象。Bitmap的内存从3.0以前的位于native,到后来改成jvm,再到8.0又改回到native。jvm每个进程都有内存上限,而native则没有限制(不是没有影响,至少不会oom),所以把内存大户Bitmap挪到native可能是很多人的梦想,但native的管理和实现明显比jvm更为复杂,除非有现成实现,很少有人去动这一块。

二、初识Bitmap

        Bitmap是一个final类,因此不能被继承。Bitmap只有一个构造方法,且该构造方法是没有任何访问权限修饰符修饰,也就是说该构造方法是friendly,但是谷歌称Bitmap的构造方法是private(私有的),感觉有点不严谨。不管怎样,一般情况下,我们不能通过构造方法直接新建一个Bitmap对象。

        Bitmap是Android系统中的图像处理中最重要类之一。Bitmap可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等 *** 作,并可以以指定格式保存图像文件。

三、创建Bitmap对象

既然不能直接通过构造方法创建Bitmap,那怎样才能创建Bitmap对象。通常我们可以利用Bitmap的静态方法createBitmap()和BitmapFactory的decode系列静态方法创建Bitmap对象。

Bitmap的静态方法createBitmap()
BitmapFactory的decode系列静态方法
四、Bitmap的颜色配置信息与压缩方式信息

        Bitmap中有两个内部枚举类:Config和CompressFormat,Config是用来设置颜色配置信息的,CompressFormat是用来设置压缩方式的。

Config解析: Bitmap.Config.ALPHA_8:颜色信息只由透明度组成,占8位。Bitmap.Config.ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。Bitmap.Config.ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。Bitmap.Config.RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。

通常我们优化Bitmap时,当需要做性能优化或者防止OOM(Out Of Memory),我们通常会使用Bitmap.Config.RGB_565这个配置,因为Bitmap.Config.ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。

CompressFormat解析: Bitmap.CompressFormat.JPEG:表示以JPEG压缩算法进行图像压缩,压缩后的格式可以是".jpg"或者".jpeg",是一种有损压缩。Bitmap.CompressFormat.PNG:表示以PNG压缩算法进行图像压缩,压缩后的格式可以是".png",是一种无损压缩。Bitmap.CompressFormat.WEBP:表示以WebP压缩算法进行图像压缩,压缩后的格式可以是".webp",是一种有损压缩,质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%。美中不足的是,WebP格式图像的编码时间“比JPEG格式图像长8倍”。 五、Bitmap 图片性能优化 方式一:使用inSampleSize采样率压缩,尺寸压缩:

BitmapFactory是通过BitmapFactory.Options对图像进行 *** 作的,然后将 *** 作后的图像生成Bitmap对象或者将 *** 作后的图像用已经存在的Bitmap保存,当不能用之保存时会返回null。
BitmapFactory.Options中常用的字段有:

使用options.inJustDecodeBounds来获取原始尺寸,然后按需使用options.inSampleSize来采样图片到接近view尺寸。

BitmapFactory 在解码图片时,可以带一个Options,有一些比较有用的功能,比如:

inTargetDensity:绘制到目标Bitmap上的像素密度。inSampleSize:对图像进行压缩,这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的整数次幂或者接近2的整数次幂。例如,width=100,height=100,inSampleSize=2(设置为2时),那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4inJustDecodeBounds:如果设置成true,表示获取Bitmap对象信息,但是不将其像素加载到内存。有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片。inPreferredConfig:Bitmap对象颜色配置信息,默认会使用Bitmap.Config.ARGB_8888,在这个模式下一个像素点将会占用4个byte(8+8+8+8=32位(4字节)),而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte(5+6+5=16位(2字节)),一下可以省下50%内存。inPurgeableinInputShareable这两个需要一起使用,BitmapFactory.java的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个bitmap,有点类似软引用,但是实际在5.0以后这两个属性已经被忽略,因为系统认为回收后再解码实际会反而可能导致性能问题inBitmap:官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在4.4以前只有相同大小的图片内存区域可以复用,4.4以后只要原有的图片比将要解码的图片大既可以复用了。inDensity:给Bitmap对象设置的密度,如果inScaled为true(这是默认的),而若inDensityinTargetDensity不匹配,那么就会在Bitmap对象返回前将其缩放到匹配inTargetDensityinDither:是否对图像进行抖动处理,默认值是false。inScaled:设置是否缩放。outHeight:Bitmap对象的高度。outWidth:Bitmap对象的宽度。 方式二:合理选择Bitmap的像素格式

ARGB8888格式的图片,每像素占用 4 Byte(8+8+8+8=32位(4字节)),而 RGB565则是 2 Byte(5+6+5=16位(2字节))。如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节。

格式描述
ALPHA_8只有一个alpha通道,每个像素用一个字节(8位)存储
ARGB_4444这个从API 13开始不建议使用,因为质量太差
ARGB_8888ARGB四个通道,每个像素用四个字节(32位)存储
RGB_565每个像素占2字节,其中红色占5bit,绿色占6bit,蓝色占5bit

ALPHA8 没必要用,因为我们随便用个颜色就可以搞定的。

ARGB4444 虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃。

ARGB8888、RGB565默认会使用ARGB_8888,在这个模式下一个像素点将会占用4个byte,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte,一下可以省下50%内存。

方式三:将图片放到合适的文件夹下:

同一张图片,放在不同目录下,会生成不同大小的bitmap,因为它的宽高都被缩放了,所以图片资源应该尽可能放在高密度的资源文件夹下,这样可以节省图片的内存开支,一般建议放在xxhdpi下,目前主流手机都是这个dpi,并且UI给我们提供切图时也应该尽量面向高密度的屏幕设备来提供

jpg 是一种有损压缩的图片存储格式,而 png 则是无损压缩的图片存储格式,显而易见,jpg 会比 png 小.

Bitmap 在内存当中占用的大小其实取决于:

色彩格式,前面我们已经提到,如果是 ARGB8888 那么就是一个像素4个字节(8+8+8+8=32位(4字节)),如果是 RGB565 那就是2个字节(5+6+5=16位(2字节))

原始文件存放的资源目录(是hdpi还是 xhdpi 还是 xxhdpi 可不能傻傻分不清楚);

目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的);

方式四:设计合理的缓存机制(内存缓存,复用池,硬盘缓存)

内存缓存:可以使用官方已经帮我们实现好的LurCache;

复用池:用来装载内存缓存LRU中被抛弃掉的,但是可能又会马上使用到的那些图片,我们在复用池中缓存下来,防止立马被GC回收掉;

硬盘缓存:有 JakeWharton 大神实现的 DiskLruCache ,下载地址:https://github.com/JakeWharton/DiskLruCache

为什么要设置复用池呢?

因为在官方8.0Android系统中,将图片放在native层中进行了处理,例如回收机制,在java层我们无法进行干预,为了能够不立即将图片交给native层处理,我们利用复用池中的弱引用暂时保存在java层中,以方便我们需要使用刚刚被内存缓存LRU放弃掉的图片,而不是重复decode图片再放入到LRU队列中,降低性能消耗。

六、Bitmap 图片性能优化代码 图片优化需要做三件事情: 像素点的压缩(格式转换)内存复用三级缓存设计(内存、磁盘、网络) 像素点的压缩
// 用来优化图片(像素点的压缩(格式转换))
public class ImageResize {
    // int maxW,int maxH ———— 限定像素点的宽和高
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha) {
        Resources resources = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
//        Bitmap bitmap = BitmapFactory.decodeResource(resources,R.drawable.logo);
        // 只解码出相关参数信息(宽、高信息等),不会真实产生图片
        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) {
            // 不需要透明度,使用 RGB_565(5+6+5=16位)
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false; // 处理完进行关闭
        // 返回设置过后的真实图片
        return BitmapFactory.decodeResource(resources, id, options);
    }

    // 计算缩放系数
    // 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;
    }
}

像素点的压缩+内存复用
public class ImageResize {
    // int maxW,int maxH ———— 限定像素点的宽和高
    // reusable:内存复用位置
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha,Bitmap reusable) {
        Resources resources = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
//        Bitmap bitmap = BitmapFactory.decodeResource(resources,R.drawable.logo);
        // 只解码出相关参数信息(宽、高信息等),不会真实产生图片
        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) {
            // 不需要透明度,使用 RGB_565(5+6+5=16位)
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false; // 处理完进行关闭

        // 设置成能复用
        options.inMutable=true;
        // 设置复用需要的内存位置
        options.inBitmap = reusable;

        // 返回设置过后的真实图片
        return BitmapFactory.decodeResource(resources, id, options);
    }

    // 计算缩放系数
    // 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;
    }
}

bitmap 对象在内存中申请一块内存,bitmap2 需要使用时直接复用 bitmap 申请的内存(也可以直接覆盖掉bitmap的内容)

三级缓存机制:内存缓存、复用池、磁盘缓存(内存、磁盘、网络)

public class ImageCache {

    public static final String TAG = "ImageCache";

    private static ImageCache instance;
    private Context context;

    public static ImageCache getInstance() {
        if (null == instance) {
            synchronized (ImageCache.class) {
                if (null == instance) {
                    instance = new ImageCache();
                }
            }
        }
        return instance;
    }

    // 内存缓存
    private LruCache<String, Bitmap> memoryCache;
    // 磁盘缓存
    private DiskLruCache diskLruCache;

    BitmapFactory.Options options = new BitmapFactory.Options();

    // 复用池
    public static Set<WeakReference<Bitmap>> reuseablePool;

    // 参数 dir 就是最后存储的磁盘缓存的路径
    public void init(Context context, String dir) {

        this.context = context.getApplicationContext();
        // synchronizedSet:带锁的集合
        reuseablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());

        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass = am.getMemoryClass();// 虚拟机提供的可用内存(APP可用的内存)
        memoryCache = new LruCache<String, Bitmap>(memoryClass / 8 * 1024 * 1024) {// 1/8的可用内存用于图片的缓存(APP可用的内存里取1/8)

            /*
             *  return    value占用的内存大小
             */

            @Override
            // sizeOf方法用于计算图片大小
            protected int sizeOf(String key, Bitmap value) {
                // 为了兼容 Android 3.0以前(19以前和以后)
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                    // 大于 19 以后(可以压缩复用)
                    return value.getAllocationByteCount();
                }
                // 19 以前复用内存只能使用同样大小的图片才能复用内存
                return value.getByteCount();
            }

            @Override
            // LruCache放满后一些数据会挤出(在 oldValue 挤出)
            // newValue:队头 放入的对象
            // oldValue:队尾 挤出的对象
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                // oldValue需要放入到复用池
                if (oldValue.isMutable()) {
                    // 用 WeakReference 与 引用队列(ReferenceQueue)进行关联
                    reuseablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
                } else {
                    oldValue.recycle();
                }
            }
        };

        try {
            // 手机上打开一个目录
            // 参数三:1 个文件
            diskLruCache = DiskLruCache.open(new File(dir), 1, 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 引用队列
    ReferenceQueue referenceQueue;
    Thread clearReferenceQueue;
    boolean shutDown;

    // 用于主动监听GC的API,加快回收(可以兼容不同的Android版本)
    private ReferenceQueue<Bitmap> getReferenceQueue() {
        if (null == referenceQueue){
            referenceQueue = new ReferenceQueue<Bitmap>();
            // 单开一个线程,去扫描引用队列中GC扫到的内容,交到native层去释放
            clearReferenceQueue = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!shutDown){
                        try {
                            // remove带阻塞功能的
                            Reference<Bitmap> reference = referenceQueue.remove();
                            Bitmap bitmap = reference.get();
                            if (null!=bitmap && !bitmap.isRecycled()){
                                bitmap.recycle(); // 提高加快回收动作(转到 native 层进行回收)
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            clearReferenceQueue.start();
        }
        return referenceQueue;
    }

    /**
     * 加入内存缓存
     */
    public void putBitmapToMemeory(String key,Bitmap bitmap){
        memoryCache.put(key,bitmap);
    }

    public Bitmap getBitmapFromMemory(String key){
        return memoryCache.get(key);
    }

    public void clearMemoryCache(){
        memoryCache.evictAll();
    }

    // 获取复用池中的内容
    public Bitmap getReuseable(int w,int h,int inSampleSize){
        // 3.0以下不理会
        if (Build.VERSION.SDK_INT<Build.VERSION_CODES.HONEYCOMB){
            return null;
        }
        Bitmap reuseable = null;
        // 通过迭代器
        Iterator<WeakReference<Bitmap>> iterator = reuseablePool.iterator();
        while (iterator.hasNext()){
            Bitmap bitmap = iterator.next().get();
            if (null!=bitmap){
                // 可以复用
                if (checkInBitmap(bitmap,w,h,inSampleSize)){
                    reuseable = bitmap;
                    iterator.remove();
                    Log.i(TAG, "复用池中找到了");
                    break;
                }else {
                    iterator.remove();
                }
            }
        }
        return reuseable;
    }

    // 检测能不能复用
    private boolean checkInBitmap(Bitmap bitmap, int w, int h, int inSampleSize) {
        if (Build.VERSION.SDK_INT<Build.VERSION_CODES.KITKAT){
            return bitmap.getWidth()==w && bitmap.getHeight()==h && inSampleSize == 1;
        }
        // 缩放系数大于 1 的就是可以复用
        if (inSampleSize>=1){
            w/=inSampleSize;
            h/=inSampleSize;
        }
        int byteCount = w*h*getPixelsCount(bitmap.getConfig());
        return byteCount<=bitmap.getAllocationByteCount();
    }

    /*
     *  用于获取像素点的不同的格式所需要的字节数
     */
    private int getPixelsCount(Bitmap.Config config) {
        if (config == Bitmap.Config.ARGB_8888){
            return 4;
        }
        return 2;
    }

    // 磁盘缓存的处理
    /*
     *  加入磁盘缓存
     */
    public void putBitMapToDisk(String key,Bitmap bitmap){
        DiskLruCache.Snapshot snapshot = null;
        OutputStream os = null;
        try {
            snapshot = diskLruCache.get(key);
            // 如果缓存中已经有这个文件   不理他
            if (null==snapshot){
                // 如果没有这个文件,就生成这个文件
                DiskLruCache.Editor editor = diskLruCache.edit(key);
                if (null!=editor){
                    os = editor.newOutputStream(0);
                    bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
                    editor.commit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (null!=snapshot){
                snapshot.close();
            }
            if (null!=os){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
     *  从磁盘缓存中取
     */
    public Bitmap getBitmapFromDisk(String key,Bitmap reuseable){
        DiskLruCache.Snapshot snapshot = null;
        Bitmap bitmap = null;
        try {
            snapshot = diskLruCache.get(key);
            if (null==snapshot){
                return null;
            }
            // 获取文件输入流,读取 bitmap
            InputStream is = snapshot.getInputStream(0);
            // 解码个图片,写入
            options.inMutable = true;
            options.inBitmap = reuseable;
            bitmap = BitmapFactory.decodeStream(is,null,options);
            if (null != bitmap){
                memoryCache.put(key,bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (null!=snapshot){
                snapshot.close();
            }
        }
        return bitmap;
    }
}
完整代码

BitMapAppDemo

参考

性能优化专题三–内存优化(图片三级缓存)
玩转Android Bitmap

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/992619.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存