安卓性能优化04-图片优化

性能优化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 lruCache;
public void test(View view) {
    long maxMemory = Runtime.getRuntime().maxMemory();
    int cacheSize = (int) (maxMemory / 8);
    if (lruCache == null) {
        lruCache = new LruCache(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-图片优化)