性能优化04-图片优化

作者: 最爱的火 | 来源:发表于2018-05-16 09:49 被阅读34次

    性能优化04-图片优化

    一、图片压缩

    图片在APP中通常占用很大的内存,所以经常需要进行图片压缩。

    常用的图片压缩方式:尺寸压缩、质量压缩、格式转换。

    1.尺寸压缩

    将一张大图加载进内存时,需要先进行尺寸压缩,不然很容易导致oom。

    尺寸压缩既会影响图片的存储大小,也会影响图片的内存大小。

    尺寸压缩使用 BitmapFactory.Options,加载2次:

    1. 第一次只加载边框,获取图片尺寸;
    2. 设置压缩比例,加载完整图片。
    /**
     * 尺寸压缩
     * @param resources 资源
     * @param id        资源id
     * @param newWidth  压缩后的宽度
     * @param newHeight 压缩后的高度
     * @return 压缩后的Btmap
     */
    public static Bitmap sizeCompress(Resources resources, int id, float newWidth, float newHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        //开始读入图片,此时把options.inJustDecodeBounds 设回true了
        options.inJustDecodeBounds = true;
    
        Bitmap bitmap = BitmapFactory.decodeResource(resources, id, options);
        options.inJustDecodeBounds = false;
        int w = options.outWidth;
        int h = options.outHeight;
        //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        int sacle = (int) (w / newWidth > h / newHeight ? w / newWidth : h / newHeight);
        sacle = sacle <= 0 ? 1 : sacle;
        options.inSampleSize = sacle;//设置缩放比例
        //重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了
        bitmap = BitmapFactory.decodeResource(resources, id, options);
        return bitmap;
    }
    

    注意:压缩比例可以设置任意值,但是实际压缩比例一定是2的n次方。

    2.质量压缩

    质量压缩只会影响图片的存储大小,不会影响图片的内存大小。另外,将质量压缩后的图片转化为二进制数据进行传输时,数据也变小了。比如微信分享图片。

    Bitmap.compress()是系统提供的质量压缩工具。Bitmap.compress()使用不完整的Skia库,对jpeg的处理基于libjpeg,对png则是基于libpng。 libjpeg库压缩图片时,可设置哈夫曼编码。但由于cpu的缘故,7.0之前没开开启,7.0以后才开启。

    /**
     * 质量压缩
     * @param bitmap         原图
     * @param compressFormat 图片类型:JPEG,PNG,WEBP;
     * @param quality        质量
     * @return 压缩后的Bitmap
     */
    public static Bitmap qualityCompress(Bitmap bitmap, Bitmap.CompressFormat compressFormat, int quality) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(compressFormat, quality, baos);
        byte[] bytes = baos.toByteArray();
        Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        return bm;
    }
    

    另外,介绍一个第三方的质量压缩框架LibJpeg-turbo。下载地址:https://libjpeg-turbo.org/

    3.格式转换

    安卓常用的图片格式有JPEG,PNG和WEBP。

    JPEG是一种针对照片视频而广泛使用的一种压缩标准方法。

    • 常用的.jpg文件是有损压缩
    • 不支持背景透明
    • 适用于照片等色彩丰富的大图压缩
    • 不适用于logo,线图

    PNG即便携式网络图形(Portable Network Graphics,PNG)是一种无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。Android开发中的切图素材多为.png格式。

    • 支持256色调色板技术以产生小体积文件
    • 最高支持48位真彩色图像以及16位灰度图像。
    • 支持Alpha通道的透明/半透明特性。
    • 支持图像亮度的Gamma校准信息。
    • 支持存储附加文本信息,以保留图像名称、作者、版权、创作时间、注释等信息。
    • 使用无损压缩。
    • 渐近显示和流式读写,适合在网络传输中快速显示预览效果后再展示全貌。
    • 使用CRC防止文件出错。
    • 最新的PNG标准允许在一个文件内存储多幅图像。

    WEBP是一种同时提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式VP8,是由Google在购买On2 Technologies后发展出来,以BSD授权条款发布。Android 4.0+默认支持WebP,Android 4.2.1+开始支持无损WebP和带alpha通道的WebP。

    • 具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量
    • 无损的WebP图片比PNG小26%,有损的WebP图片比JPEG小25-34%
    • 相较编码JPEG文件,编码同样质量的WebP文件也需要占用更多的计算资源
    • 具备了无损和有损的压缩模式、Alpha 透明以及动画的特性
    • 在 JPEG 和 PNG 上的转化效果都非常优秀、稳定和统一
    • 无损WebP支持透明及alpha通道,有损在一定条件下同样支持

    如果不考虑兼容性,那么使用有损的WebP图片,内存最省;使用无损的WebP图片,性能最优。

    另外,将PNG和JPEG转换层WEBP格式,可以在AndroidStudio上,选中图片,右键选择Converting Images to Webp来进行转换。

    如果考虑兼容性,那么使用JPG,内存更省;使用PNG,效果最好。PNG和JPEG的转换有很多工具,这里就不说了。

    二、色彩模式

    图片在内存中的大小,由像素和色彩模式决定。

    色彩模式可以说是每个像素所占的字节数,决定了图片的精细度。常见的色彩模式:RGB_565、ARGB_4444、ARGB_8888。

    ARGB的含义:A代表alpha 通道,即透明度;R代表Red,红色;G代表Green,绿色;B代表Blue,蓝色。

    RGB_565:R占5位,G占6位,B占5位,共16位,即2个字节;

    ARGB_4444:A占4位,R占4位,G占4位,B占4位,共16位,即2个字节;

    ARGB_8888:A占8位,R占8位,G占8位,B占8位,共32位,即4个字节;

    RGB_565和ARGB_4444的所占内存小于ARGB_8888,精细度也低于ARGB_8888。不过从肉眼,很难看出三者的差别。

    /**
     * 设置图片色彩模式
     *
     * @param bitmap 位图
     * @param config 色彩模式,常用的:RGB_565,ARGB_4444ARGB_8888。
     * @return 返回图片大小
     */
    public static long setConfig(Bitmap bitmap, Bitmap.Config config) {
        if (bitmap == null) {
            return 0;
        }
        Bitmap newBitmap = bitmap.copy(config, true);
        long size = newBitmap.getByteCount();
        return size;
    }
    

    三、图片缓存

    展示图片时,经常需要重新加载图片。对于重复加载的图片,如果使用缓存,就会提高性能。

    1.内存缓存

    当界面刷新时,就会重新加载图片。如果重新创建一个Bitmap对象,就很耗性能,这个时候可以使用LruCache来缓存Bitmap对象。

    LruCache<String, Bitmap> lruCache;
    public void test(View view) {
        long maxMemory = Runtime.getRuntime().maxMemory();
        int cacheSize = (int) (maxMemory / 8);
        if (lruCache == null) {
            lruCache = new LruCache<String, Bitmap>(cacheSize){  
            //必须重写此方法,来测量Bitmap的大小  
            @Override  
            protected int sizeOf(String key, Bitmap value) {  
                return value.getRowBytes() * value.getHeight();  
            };         
        }
        Bitmap bitmap = lruCache.get("T");
        if (bitmap == null) {
            bitmap = ToolBitmap.sizeCompress(getResources(), R.mipmap.wangyiyun_icon, 400, 400);
            lruCache.put("T", bitmap);
        }
        ImageView img = findViewById(R.id.img);
        img.setImageBitmap(bitmap);
    }
    

    LruCache使用了Lru算法(最近使用的优先级最高,最少使用的优先级最低),通过LinkedHashMap来存取对象,每次取数据后,把它放在链表最后面;每次添加数据时,也是放在链表最后面,同时检查缓存大小,如果超过阀值,就移除链表最前面的数据。

    2.文件缓存

    当Activity重新启动时,可能需要展示之前的图片,这是时候就需要文件缓存,我们使用第三方的DiskLruCache。

    Github地址:https://github.com/JakeWharton/DiskLruCache

    使用参考:https://www.jianshu.com/p/0c56dc217917

    3.Bitmap复用

    Bitmap复用:不再使用的Bitamp可分配给其他图片使用。

    通过bitmap复用,减少频繁申请内存带来的性能问题。

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inMutable = true;//必须设为true,否则不能被复用
    //被复用的bitmap
    Bitmap oldbitmap = BitmapFactory.decodeResource(getResources(),R.drawable.lance,options);
    
    //复用oldbitmap
    options.inBitmap = oldbitmap;
    Bitmap newbitmap = BitmapFactory.decodeResource(getResources(),R.drawable.lance,options);
    

    在4.4之前,Bitmap格式必须为jpg、png,inSampleSize为1才能复用,并且复用的bitmap与被复用的bitmap同等宽高。

    在4.4以后:被复用的Bitmap的内存必须大于等于复用的bitmap的内存。

    四、超大图加载

    超大图加载的核心是BitmapRegionDecoder。BitmapRegionDecoder主要用于显示图片的某一块矩形区域。

    BitmapRegionDecoder的使用很简单:

    //isShareable为false会复制一张图片,为true会共用
    BitmapRegionDecoder Decoder = BitmapRegionDecodeBitmapRegionDecoder r.newInstance(is, false);
    //获取指定区域的bitmap。mRect为区域的矩阵,mOptions位图片的配置
    Bitmap mBitmap = mDecoder.decodeRegion(mRect, mOptions);
    

    我们通常加载超大图,都是使用自定义控件。自定义控件有两个关键:

    1. 使用BitmapRegionDecoder加载图片;
    2. 重写事件监听,实现图片拖动;

    下面贴出关键代码:

    /**
     * 设置图片
     *
     * @param is 图片的输入流
     */
    public void setImage(InputStream is) {
        mOptions.inJustDecodeBounds = true;//加载边框
        BitmapFactory.decodeStream(is, null, mOptions);//获取图片宽高
        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;
        mOptions.inMutable = true;//复用Bitmap TODO
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;//设置像素格式
        mOptions.inJustDecodeBounds = false;//加载图片
        try {
            mDecoder = BitmapRegionDecoder.newInstance(is, false);//isShareable为false会复制一张图片,为true会共用
        } catch (IOException e) {
            e.printStackTrace();
        }
        requestLayout();//重新布局
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mDecoder == null) {
            return;
        }
    
        mViewWidth = getMeasuredWidth();//测量控件的宽
        mViewHeight = getMeasuredHeight();//测量控件的高
        mSacle = (float) mViewWidth / mImageWidth;
    
        //获取加载区域
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = mImageWidth;
        mRect.bottom = (int) (mViewHeight / mSacle);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDecoder == null) {
            return;
        }
        mOptions.inBitmap = mBitmap;//复用mBitmap
        mBitmap = mDecoder.decodeRegion(mRect, mOptions);
        Matrix matrix = new Matrix();//通过矩阵缩放
        matrix.setScale(mSacle, mSacle);//设置水平和垂直方向的缩放
        canvas.drawBitmap(mBitmap, matrix, null);
    }
    
    /**
     * 重写onScroll,调整图片的矩阵显示区域
     */
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        mRect.offset(0, (int) distanceY);//垂直滑动图片
        if (mRect.bottom > mImageHeight) {
            mRect.top = (int) (mImageHeight - mViewHeight / mSacle);
            mRect.bottom = mImageHeight;
        }
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mSacle);
        }
        invalidate();//重绘
        return false;
    }
    

    最后

    代码地址:https://gitee.com/yanhuo2008/Common/blob/master/Tool/src/main/java/gsw/tool/ui/BigView.java

    性能优化专题:https://www.jianshu.com/nb/25128595

    喜欢请点赞,谢谢!

    相关文章

      网友评论

        本文标题:性能优化04-图片优化

        本文链接:https://www.haomeiwen.com/subject/fooudftx.html