Android开发-自己动手写Bitmap高效加载 跟OOM说再见

版权声明:本文为博主原创文章,转载请标明出处!如果文章有错误之处请留言 https://blog.csdn.net/qq_30993595/article/details/84202510

前言

Bitmap三连

结合Bitmap三级缓存自己做个ImageLoader 解决UI卡顿问题
Android之带你从源码解析Bitmap占用内存正确的计算公式
自己动手写Bitmap高效加载 跟OOM说再见

从前面两篇文章写下来,不得不继续的一个问题就是如何高效加载Bitmap,如何不让我们的应用因为Bitmap,或者说因为加载大图片而产生OOM,怎么才能让我们放心大胆的在APP中使用Bitmap,不需要整天提心吊胆的担心出Bug;这篇文章就来解决这个问题

本文所含代码随时更新,可从这里下载最新代码
传送门

BitmapFactory.Options

说到加载Bitmap,肯定离不开BitmapFactory的静态内部类Options,这个类的配置参数如何影响Bitmap的加载,可以从BitmapFactory.cpp这个native文件的doDecode方法知悉,如果你看过上面列出的第二篇文章你就懂了;总的来说就是BitmapFactory.cpp对图片进行解码时使用的一个配置参数类

配置参数如下:

  • inBitmap:这个属性是从API11才有的,如果使用Options对象设置了这个参数,那么解码器将会重用此Bitmap,不再重新开辟内存,这就意味着多个Bitmap可以使用同一块内存,显然这是可以提升性能的一点,但是它有一些注意点
    1. 在Android4.4(API19)之前,要求正在解码的图像必须是JPG或者PNG格式,无论是从resource还是stream获取;并且正在解码的Bitmap和待重用的Bitmap必须是相同大小的,同时inSampleSize值要求设置为1;另外待重用的Bitmap的inPreferredConfig属性将会覆盖正在解码的Bitmap的这个属性
    2. 从Android4.4(API19)开始,只需要正在解码的Bitmap的size小于或等于待重用的Bitmap即有效
    3. 如果解码器不能操作待重用的Bitmap,解码结果将为null并抛出IllegalArgumentException,除非您特别确定待重用的Bitmap没有问题,否则避免使用
  • inMutable:这个属性是从API11才有的,设置后,将会获得一个可变的Bitmap,可以修改bitmap’s pixels,使用Bitmap.copy(Config config, boolean isMutable)等方法可以修改BitmapFactory解码后的Bitmap
  • inJustDecodeBounds:设置为true,解码器将不会返回Bitmap,主要为调用者查询Bitmap信息而不为其像素分配内存;默认为false,会解码图片,并分配相应的内存
  • inSampleSize:用来Bitmap的采样值,如果大于1,解码器将会对Bitmap进行二次采样,返回一个较小的Bitmap以节省内存;如果小于等于1,将被重置为1;比如设置为2,返回的Bitmap的宽高都变成原始值的1/2,像素数为1/2*1/2即1/4;有一点需要注意:解码器使用基于2的幂的最终值,任何其他值将向下舍入到最接近的2的幂
  • inPreferredConfig:设置图片解码时使用的颜色格式,也就是每个像素的存储方式,有四个值ARGB_8888,ARGB_4444,RGB_565,ALPHA_8,默认这个选项的值是ARGB_8888;不过有几点需要注意:
    1. 如果inPreferredConfig值为null,解码时使用的颜色格式会根据图片源文件格式选取
    2. 如果inPreferredConfig值不为null,解码时发现无法满足此参数指定的颜色格式,那解码器将不会根据图片源文件格式选取,而是直接使用ARGB_8888
  • inPremultiplied:如果设置为true(默认是true),那么Bitmap每个颜色通道都会预先被乘以Alpha通道,默认为true;其中对于由系统View或者Canvas直接绘制的图像,应该设置为true,因为系统View和Canvas假设所有绘制的图像都是预乘的,以简化绘制时混合,并在绘制非预乘时抛出RuntimeException;通常情况下只有处理原始编码图像数据时才设置为false,比如使用RenderScript或自定义OpenGL;该配置不会影响没有Alpha通道的位图;同时将inScaled设置为true时将此标志设置为false可能会导致颜色不正确

  • inDither:设置为true(默认是false),解码器将尝试抖动解码图像,从API24开始,这个配置会被忽略

    要知道我们使用的图片都是被压缩的,图片按照一定规律舍去一些相似的像素中的颜色,达到降低文件大小的目的;但是解码的时候要想还原就需要补齐丢失的颜色;比如一张颜色丰富的图片,用一个位数较低的颜色格式来解码,那颜色肯定不够用的,那解码后的图片在一些颜色渐变的区域上就会有一些很明显的断裂色带,因为一些丰富的颜色在位数较低的颜色模式下并没有,那么只能用相近的填充,可能一大片都没有,那么一大片都用这一个颜色填充,如果采用抖动解码,那么就会在这些色带上采用随机噪声色来填充,目的是让这张图显示效果更好,断裂色带不那么明显

  • inDensity:Bitmap的像素密度,结合inTargetDensity用来计算Bitmap缩放比例,在上面的计算Bitmap内存文章中有详细介绍

  • inTargetDensity:当前设备屏幕的densityDpi,结合inDensity用来计算Bitmap缩放比例,在上面的计算Bitmap内存文章中有详细介绍

  • inScaled:设置为true(默认是true),如果inDensity和inTargetDensity不为0,则Bitmap将在加载时缩放以匹配inTargetDensity,而不是每次将其绘制到Canvas时依赖于图形系统缩放它;如果inPremultiplied设置为false,并且图像具有alpha,则将此标志设置为true可能会导致颜色不正确

  • inPreferQualityOverSpeed:该配置从API24开始被弃用,如果设置为true,则图片会有更高的品质,但是解码速度会很慢,仅用于JPEG格式

  • outWidth和outHeight:图片的原始宽高,通常结合inJustDecodeBounds一起使用

BitmapFactory

接下来就看加载工厂类了,它提供了一系列的API帮助开发者加载Bitmap,这里介绍一些常用的

  • Bitmap decodeFile(String pathName):将文件系统中的图片文件解码返回Bitmap
  • Bitmap decodeFile(String pathName, Options opts):比上面多了Options 配置项
  • Bitmap decodeResource(Resources res, int id):将资源目录中的图片文件解码返回Bitmap
  • Bitmap decodeResource(Resources res, int id, Options opts):比上面多了Options 配置项
  • Bitmap decodeStream(InputStream is):将图片输入流解码成Bitmap
  • Bitmap decodeStream(InputStream is, Rect outPadding, Options opts):比上面多了Options 配置项,其中outPadding用于设置padding

高效加载Bitmap

高效加载Bitmap的核心点就是在保证一定显示质量情况下怎么降低Bitmap所占的内存

那问题来了,有哪些途径可以降低Bitmap所占的内存呢?从上一篇文章我们知道图片所占内存的计算公式如下:

memory = (int)(bitmapWidth * targetDensity / density + 0.5f) * (int)(bitmapHeight * targetDensity / density + 0.5f) * ColorType

其中bitmapWidth(图片宽度),bitmapHeight (图片高度)和ColorType(单个像素占用字节数)三个因子我们可以直接控制,只要减小其中一个或几个就可以降低内存占用

接下来以decodeResource(Resources res, int id, Options opts)方法为例进行讲解

inDensity,inTargetDensity,inScaled

首先inScaled默认是true,也就是支持缩放,那缩放比例是多少呢,根据BitmapFactory.cpp中的doDecode函数可以知道
scale = (float)inTargetDensity/ inDensity,也就是说如果我们不设置inDensity和inTargetDensity的值,那么系统会自动为我们计算这个缩放比例;假如我们自己主动传入这个值,那这个缩放比就由我们自己控制了

BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = 160;
options.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.app_logo, options);
int size = bitmap.getByteCount();

自己控制值的话放大还是缩小就由你说了算,不过通常情况下为了更好的支持多屏幕还是交给系统去做吧,除非你很有把握控制多屏幕显示效果

这种方法减小图片的宽高以减少图片像素数达到降低内存占用

inSampleSize,inJustDecodeBounds

通常情况下这两个值会结合起来用,也是用的最多的一种了;inSampleSize指定采样比例,inJustDecodeBounds用来在为Bitmap分配内存前拿到图片的原始宽高,然后根据图片宽高和指定的显示宽高计算缩放比例;当inSampleSize指定为n时,图片宽高都会被压缩到1/n,像素数据被压缩到n的平方分之一

这里因为采样会减少样本数据规模,比较适合于图片本身较大,或者对图片分辨率,大小要求不是非常严格的情况

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap value = BitmapFactory.decodeResource(getResources(), R.mipmap.img_nav_01,options);

这里直接指定缩放比,解码后的图片宽高都变成原始宽高的1/4,像素数变成了1/16

但是通常情况下我们会指定图片显示控件的宽高,但是为了保证图片显示不被拉伸,我们应当让压缩后的图片宽高都不小于指定显示的宽高,这样就得去计算缩放比

public static Bitmap decodeResourceBitmap(Resources res, int resId,int targetWidth, int targetHeight) {

        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        options.inSampleSize = calSampleSize(options, targetWidth, targetHeight);

        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
    
	/**
     * 根据指定宽高计算压缩比
     * @param options
     * @param targetWidth
     * @param targetHeight
     * @return
     */
    public static int calSampleSize(BitmapFactory.Options options,
                                    int targetWidth, int targetHeight) {
        int rawWidth = options.outWidth;
        int rawHeight = options.outHeight;
        int inSampleSize = 1;
        //保证宽高中最小的要大于指定值,避免被拉伸
        if (rawWidth > targetWidth || rawHeight > targetHeight) {
            float ratioHeight = (float) rawHeight / targetHeight;
            float ratioWidth = (float) rawWidth / targetWidth;
            inSampleSize = (int) Math.min(ratioWidth, ratioHeight);
        }
        return inSampleSize;
    }

当设置options.inJustDecodeBounds = true后,解码器不会为Bitmap分配内存,只会拿到原始图片的信息;计算比例后再设置options.inJustDecodeBounds = false,这样解码器就会真正去解码图片

这种方法同样是减小图片的宽高以减少图片像素数达到降低内存占用

inPreferredConfig

上面说的几种压缩都是采样率压缩,像素数和图片尺寸都压缩了,但是有时候你不希望尺寸变化,只希望所占内存降低,那就可以通过改变每个像素的颜色格式,达到减少单个像素占用的内存而不改变图片原有尺寸(也就是改变ColorType的值),那就通过inPreferredConfig设置

 /**
     * 修改像素颜色格式压缩图片
     * @param res
     * @param resId
     * @param inPreferredConfig
     * @return
     */
    public static Bitmap decodeResourceBitmap(Resources res, int resId,Bitmap.Config inPreferredConfig){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = inPreferredConfig;
        return BitmapFactory.decodeResource(res,resId,options);
    }

Matrix

说到Bitmap的处理肯定离不开Matrix,Matrix中文理解是矩阵,一说起矩阵就想到了大学学习 高等代数与解析几何 这门课,算了,那是个忧伤的时刻,不想再回忆矩阵变换了;再回到Bitmap吧,因为Bitmap的像素点阵就是个矩阵,所以在进行Bitmap变换的时候可以通过Matrix来操作,其中Canvas提供了一个方法 接收一个Matrix

void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)

通过采样压缩确实可以降低Bitmap占用内存,但是会缩小它的尺寸,如果这时候你在自定义View的时候需要用Canvas绘制这个图怎么弄呢,或者要把它的尺寸可以随意变化的去绘制怎么弄呢?总不能再去加载其它比例的Bitmap吧;这时候就要通过Matrix这个类了,利用它可以完成Bitmap的放大缩小,旋转错切等效果,但是Bitmap占用的内存和本身的宽高并没有变化

Matrix matrix = new Matrix();
matrix.preScale(2,2,0,0);
canvas.drawBitmap(mBitmap,matrix,mPaint);

这就能将图片放大2倍了

如果不是自定义View,也可以直接通过ImageView设置

Matrix matrix = new Matrix();
matrix.postScale(2,2,0,0);
view.setImageMatrix(matrix);
view.setScaleType(ImageView.ScaleType.MATRIX);
view.setImageBitmap(bitmap);

JPG图片 or PNG图片

大家应该知道在Android里,我们基本上用的图片都是png格式的,而很少选用jpg格式,那是为什么呢?

从表面上看同一张jpg确实要比png的size要小很多,毕竟PNG是无损压缩格式,JPG是有损压缩格式,且压缩比很大;但是这里的size 是文件系统中的概念,你就把它们俩想象成两个压缩包,压缩后的大小是你们在SD卡上看到的,但是在软件中解码后获得的大小可就差别太大了

同时JPG没有alpha通道,且JPG不适用于所含颜色较少,同时具有大块颜色相近的区域或亮度差异十分明显的图片,但是我们APP中大部分的都是图标这种小图片,很显然jgp不能很好完成这个任务

并且JPG的图像压缩算法比PNG要耗时的多,非常消耗CPU,为了用户考虑还是使用PNG吧

但是如果图片颜色非常丰富,需要高保真的复杂图片,虽然PNG能无损压缩,但是文件比JPG大的多,这时候应该考虑JPG

当然有的公司为了极限缩小APK的大小,会用SVG去代替PNG,这里就不再叙述了

自定义View

在有些业务场景中并不一定每个图片都需要UI去切图,我们完全可以自己去绘制;比如一些Loading图,通常只需要几张就够了,而且颜色也很简单黑白灰搞定,最多加点透明度;这时候你要是去加载资源图片,那真的有点费内存,还增大了APK大小;所以就自定义个View,重写onDraw方法,画一下就得了

有的时候可能要给用户展示同一张图片的不同饱和度的情况下的样子,这完全可以自己通过代码操作,千万不要去加载资源图片,很费内存啊

//图片灰阶处理
//创建颜色变换矩阵
ColorMatrix colorMatrix = new ColorMatrix();
//设置饱和度为0,实现灰阶效果
colorMatrix.setSaturation(0.5f);
//创建颜色过滤矩阵
ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(colorMatrix);
//设置画笔的颜色过滤矩阵
mPaint.setColorFilter(colorFilter);
//使用处理后的画笔绘制图像
canvas.drawBitmap(mBitmap, 0, 0, mPaint);

缓存

做Bitmap加载,缓存肯定是少不了的,适当的缓存可以避免多次加载Bitmap而开辟多余的内存,这个知识点可以参考博主前面开头贴出的一片文章,主要是利用三级缓存机制

矩阵参考博客

https://www.cnblogs.com/wgwyanfs/p/7306405.html
https://blog.csdn.net/jarlen/article/details/44877961

猜你喜欢

转载自blog.csdn.net/qq_30993595/article/details/84202510