昨天,测试说,APP的圈子列表里面,如果用户是在网页端发送的圈子动态,并且全是图片,在 APP 端加载会非常卡。因为在网页端图片没有经过压缩出来,都是原图,一张图片可能就 10多兆,而在 APP 端所有上传的 图片都是经过 鲁班 这个第三方库进行压缩,不会出现这种问题。
我们的文件服务器采用的阿里 OSS,只需要修改图片URL拼接参数,就可以得到比较小的缩略图,另外配合 Glide 也可以实现不卡顿。下面总结一下图片相关性能优化点。
这里有一个问题需要清楚:
Q1:一张图片需要的内存空间是如何计算的?
图片的内存大小是如何计算的,这里有一个通用公式:图片分辨率 * 每个像素点所占内存的大小,但这个公式不具体,这里先略过。
图片分辨率,这个很好理解,这里我们找一张测试图片,如下图所示:
我们用鼠标右键查看样式,可以很直观了解到这张图片的分辨率为 640 * 640;
我们知道上面那个通用公式前一个参数,那么后面那个参数是怎么回尼。在此之前我们要了解几个非常重要的单位概念。
单位 | 简介 |
---|---|
dp | 1. 独立像素,又称与设备无关像素,1dp = 1个 160 dpi的屏幕上一个物理像素的长度(1dp = 1px);2.160dpi 屏幕被 Andoid 定义为基准屏幕(mdpi)。3.最终要转换成像素来衡量大小的 |
dpi | 1.每英寸多少个像素点,衡量手机屏幕好坏的标准;2.我的手机是小米MIX3,手机屏幕分辨率为 2340 * 1080,手机为 6.39 英寸(对角线),那么 dpi = 234 0 2 108 0 2 / 6.39 \sqrt{2340^2 1080^2} \quad / 6.39 2340210802/6.39 = 403 ,那么在我手机上 1dp = 403 / 160 = 2.5px;4.像素密度 |
px | 屏幕上像素点 |
density | dpi / 160 |
我们可以得出这三者之间关系:px = dp * (dpi / 160) = dp * density
密度 | ldpi | mdpi(基准) | hdpi | xhdpi | xxhdpi | xxxhdpi |
---|---|---|---|---|---|---|
密度值(屏幕密度) | 120 | 160 | 240 | 320 | 480 | 640 |
代表分辨率 | 240*320 | 320*480 | 480*800 | 720*1280 | 1080*1920 | 2160*3840 |
desity | 0.75 | 1 | 1.5 | 2 | 3 | 4 |
上表中我们可以看出在 xhdpi 密度下,1dp = 2px,以此类推。
通常我们使用图片有三种方式:
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
我们来看看 decodeResource 源码里面干了什么
//最终调用 BitmapFactory.java 的 decodeResourceStream
@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
//设置默认的规格参数
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//1.inDensity 默认为图片所在文件夹的密度
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//2.inTargetDensity 当前系统密度
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
//我们发现最终调用 BitmapFactory,cpp 的 doDecode
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
.....
//缩放系数
float scale = 1.0f;
....
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
//1.获取 density
const int density = env->GetIntField(options, gOptions_densityFieldID);
//2.获取 targetDensity
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
//缩放系数 = 当前设备系数密度/图片所在文件夹的密度;
scale = (float) targetDensity / density;
}
}
//获取 Bitmap 原始宽高
int scaledWidth = size.width();
int scaledHeight = size.height();
//使用缩放系数进行缩放,缩放后的宽高;
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale 0.5f);
scaledHeight = int(scaledHeight * scale 0.5f);
}
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
canvas.scale(sx, sy);
canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
// 创建 java 的 bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
上面代码看出加载 res 资源图片,占用内存 = width * height * inTargetDensity/inDensity * inTargetDensity/inDensity 一个像素所占的内存大小。
接下来,我们将前面用到的图片(640 * 640),放到 drawable-xhdpi 目录下
//不做处理,默认
val options = BitmapFactory.Options()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.test,options)
Log.i(TAG, "byteCount: " bitmap.byteCount)
Log.i(TAG, "allocationByteCount:" bitmap.allocationByteCount)
Log.i(TAG, "width:" bitmap.width)
Log.i(TAG, "height:" bitmap.height)
Log.i(TAG, "inDensity:" options.inDensity)
Log.i(TAG, "inTargetDensity:" options.inTargetDensity)
//手动设置 inDensity、inTargetDensity,影响缩放比例
val options = BitmapFactory.Options()
options.inDensity = 640
options.inTargetDensity = 640
//输出 1
//byteCount: 3097600 //所占内存大小 = 880 * 880 * (440 / 320) *(440 * 320) * 4
//allocationByteCount:3097600
//width:880 640 * (440 / 320)
//height:880 640 * (440 / 320)
//inDensity:320
//inTargetDensity:440
//输出 1
//byteCount: 1638400 //所占内存大小 = 640 * 640 * 1 * 1 * 4
//allocationByteCount:1638400
//width:640
//height:640 //手动设置 inDensity、inTargetDensity 比例不变
//inDensity:640
//inTargetDensity:640
上面是文件存放在 res 中,假如将文件存放到磁盘中有会怎么样。
val bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().path File.separator "test.jpg")
Log.i(TAG, "byteCount: " bitmap.byteCount)
Log.i(TAG, "allocationByteCount:" bitmap.allocationByteCount)
Log.i(TAG, "width:" bitmap.width)
Log.i(TAG, "height:" bitmap.height)
//byteCount: 1638400 //所占内存大小 = 640 * 640 * 4
//allocationByteCount:1638400
//width:640
//height:640
从上面代码不让看出,只是少了 density 和 inTargetDensity(与缩放系数相关)。
因此我们可以得出以下结论:
那么问题来了,一个像素所占内存大小是如何得到的。这个很好理解,系统规定的,Bitmap.Config 用来描述图片的像素:
从上面的分析,我们的得出图片内存优化两个方向:
目前 Android 常用图片格式有三种:
采样率压缩是通过设置 BitmapFactory.Options.inSampleSize,设置的inSampleSize 会导致压缩的图片的宽高都为 1/inSampleSize,但是使用时候要注意一下两点:
/**
* @param inSampleSize 可以根据需求计算出合理的 inSampleSize
*/
public static Bitmap compress(int inSampleSize,String file) {
BitmapFactory.Options options = new BitmapFactory.Options();
//设置此参数是仅仅读取图片的宽高到 options 中,不会将整张图片读到内存中
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeFile(file, options);
options.inJustDecodeBounds = false;
//设置采样率
options.inSampleSize = inSampleSize;
bitmap = BitmapFactory.decodeFile(file, options);
return bitmap;
}
注意,质量压缩并不会改变图片在内存中的大小,仅仅会减小图片所占用的磁盘空间的大小,原理是通过改变图片的位深和透明度来减小图片占用的磁盘空间大小。
public static Bitmap compress(Bitmap bitmap){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//参数quality: 图像压缩率,0-100。 0 压缩100%,100 意味着不压缩
//参数stream: 写入压缩数据的输出流。
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, baos);
byte[] bytes = baos.toByteArray();
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
通过减少图片的像素来降低图片的磁盘空间大小和内存大小,可以用于缓存缩略图
public static Bitmap compress(Bitmap bitmap){
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
retutn Bitmap.createBitmap(bit, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, true);
}
使用 Bitmap.Config.RGB_565 替换默认的 ARGB_8888。
Bitmap.createBitmap(bitmap.getWidth() / radio, bitmap.getHeight() / radio, Bitmap.Config.ARGB_8888)
在很多图片类的 APP 中,常常会有超大图片的加载,比如以前接触到一款博物馆的 APP,里面有些图片超级大,大都是一些超级长的画。实物可能有七八米长,三四米宽,将这些超级大,超级长的画展示在手机屏幕上,一次性加载出来,显然不合适。
解决方法就是,只加载图片的局部区域,这部分区域适配屏幕大小,配合手势移动的时候更新显示对应的区域。Android 中提供 BitmapRegionDecoder 来进行图片的局部解析,具体可以通过查找相关资料。