性能优化04-图片优化
一、图片压缩
图片在APP中通常占用很大的内存,所以经常需要进行图片压缩。
常用的图片压缩方式:尺寸压缩、质量压缩、格式转换。
1.尺寸压缩
将一张大图加载进内存时,需要先进行尺寸压缩,不然很容易导致oom。
尺寸压缩既会影响图片的存储大小,也会影响图片的内存大小。
尺寸压缩使用 BitmapFactory.Options,加载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);
我们通常加载超大图,都是使用自定义控件。自定义控件有两个关键:
- 使用BitmapRegionDecoder加载图片;
- 重写事件监听,实现图片拖动;
下面贴出关键代码:
/**
* 设置图片
*
* @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
喜欢请点赞,谢谢!