关于图片的那点事儿
Q: 一张大小为 55KB, 分辨率为 1080 * 480 的 PNG 图片,它加载近内存时所占的大小是多少呢?
图片内存大小
图片占用内存大小 = 分辨率 * 像素点大小
其中数据格式不同像素点大小也不同:
- ALPHA_8: 1B
- RGB_565: 2B
- ARGB_4444: 2B
- ARGB_8888: 4B
- RGBA_F16: 8B
现在回过头来看上面的问题,在电脑上显示 55KB 的图片,png 只是这张图片的容器,他们是经过相对应的压缩算法将原图的每个像素点信息转换为另一种数据格式。
在一般情况下,这张图片占用的内容应该是:1080 * 480 * 4B = 1.98 M。
每种设备都会有所差异,以 android 为例,我们将同一张图片放在不同 dpi 的 res/drawable 目录下,占用的内存也不一样。
这是因为在 android 中 Bitmap.decodeResource()
会根据图片存放的目录做一次宽高的转换,具体公式如下:
转换后高度 = 原图高度 * (设备的 dpi /目录对应的 dpi )
转换后宽度 = 原图宽度 * (设备的 dip / 目录对应的 dpi)
假设你的手机 dpi 是 320(对应 xhdpi),你将上述的图片放在 xhdpi 目录下:
图片占用内存 = 1080 * (320 / 320) * 480 * (320 / 320) * 4B = 1.98 M
同样的手机,将上述图片放到 hdpi (240 dpi) 目录下:
图片占用内存 = 1080 * (320 / 240) * 480 * (320 / 240) * 4B = 3.52 M
如果需要查看手机 density 相关配置,可以使用如下命令:
adb shell cat system/build.prop|grep density
该命令可得到手机的 dpi,平常我们在布局中的单位都是 dp,那 1 dp 等于多少 px 呢。
根据官方转换公式在 160 dpi 手机下 1 dp 等于 1 px,如果手机 dpi 为 440 dpi,则 1 dp = 2.75 px
如何降低一张图片占用的内存
Bitmap 相关属性说明
简单了解下BitmapOption的几个相关属性:
- inBitmap——在解析Bitmap时重用该Bitmap,不过必须等大的Bitmap而且inMutable须为true
- inMutable——配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段
- inJustDecodeBounds——为true仅返回Bitmap的宽高等属性
- inSampleSize——须>=1,表示Bitmap的压缩比例,如:inSampleSize=4,将返回一个是原始图的1/16大小的
- Bitmap
- inPreferredConfig——Bitmap.Config.ARGB_8888等
- inDither——是否抖动,默认为false
- inPremultiplied——默认为true,一般不改变它的值
- inDensity——Bitmap的像素密度
- inTargetDensity——Bitmap最终的像素密度
- inScreenDensity——当前屏幕的像素密度
- inScaled——是否支持缩放,默认为true,当设置了这个,Bitmap将会以inTargetDensity的值进行缩放
- inPurgeable——当存储Pixel的内存空间在系统内存不足时是否可以被回收
- inInputShareable——inPurgeable为true情况下才生效,是否可以共享一个InputStream
- inPreferQualityOverSpeed——为true则优先保证Bitmap质量其次是解码速度
- outWidth——返回的Bitmap的宽
- outHeight——返回的Bitmap的高
- inTempStorage——解码时的临时空间,建议16*1024
降低分辨率
android 系统提供了相应的 api 可以按比例压缩图片 BitmapFactory.Options.inSampleSize
inSampleSzie 值越大,压缩比例越高
改变数据格式
android 系统默认以 ARGB_8888 格式处理图片,那么每个像素点就需要占用 4B 大小,可以将格式改为 RGB_565
Glide 中的图片压缩
图片加载的简单过程
我们使用 Glide 加载图片的最后一步是 #into(ImageView)
我们直接定位到 RequestBuilder#into(ImageView)
方法:
BaseRequestOptions> requestOptions = this;
... // 根据 ImageView 原生的 scale type 构建 Glide 的 scale type
Request = buildRequest(target, targetListener, options) // 这里最终调用的是 SingleRequest.obtain() 来创建 request
requestManager.track(target, request); //从这里开始请求 URL 加载图片
复制代码
在 tarck() 方法中执行了 targetTracker.track(target),而这行代码就是用来跟踪生命周期的
如果我们是从网络加载图片,当图片下载成功后会回调 SingleRequest#onResourceReady(Resource> resource, DataSource dataSource)
方法。
而图片的下载及解码起始于 SingleRequest#onSizeReady
,然后调用 Engine#load()
开始下载及解码:
... //省略分别从内存,disk 读取图片代码
EnginJob engineJob = engineJobFactory.build();
DecodeJob decodeJob = decodeJobFacotry.build();
josbs.put(key, enginJob);
engineJob.addCallback(cb);
engineJob.start(decodeJob); //开始解码工作
复制代码
最后调用 DecodePath#decodeResourceWithList()
,关键代码:
Resource result = null;
for (int i = 0, size = decoders.size(); i < size; i++) {
ResourceDecoder decoder = decoders.get(i);
result = decoder.decode(data, width, height, options);
}
return result;
复制代码
图片解码
接下来分析图片的解码过程。
首先我们需要搞清楚 decoders 是怎么来的,原来在初始化 Glide 时会将 Glide 支持的所有 Decoder 注册到 decoderRegistry 中,最终调用 ResourceDecoderRegistry#getDecoders()
方法来获取所需要的 decoders:
public synchronized List> getDecoders(@NonNull Class dataClass,
@NonNull Class resourceClass) {
List> result = new ArrayList<>();
for (String bucket : bucketPriorityList) {
List> entries = decoders.get(bucket);
if (entries == null) {
continue;
}
for (Entry, ?> entry : entries) {
if (entry.handles(dataClass, resourceClass)) {
result.add((ResourceDecoder) entry.decoder);
}
}
}
// TODO: cache result list.
return result;
}
复制代码
在Glide中 ResourceDecoder 的实现类有很多,如下图所示
Glide 根据图片的资源类型会调用不同的 Decoder 进行解码,现在我们以最常见的场景,加载网络图片来说明。加载网络图片(PNG格式)调用的是 ByteBufferBitmapDecoder。
不管是加载网络图片还是加载本地资源,都是通过 ByteBufferBitmapDecoder 类进行解码
public class ByteBufferBitmapDecoder implements ResourceDecoder<ByteBuffer, Bitmap> {
private final Downsampler downsampler;
public ByteBufferBitmapDecoder(Downsampler downsampler) {
this.downsampler = downsampler;
}
@Override
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
return downsampler.handles(source);
}
@Override
public Resource decode(@NonNull ByteBuffer source, int width, int height,
@NonNull Options options)
throws IOException {
InputStream is = ByteBufferUtil.toStream(source);
return downsampler.decode(is, width, height, options);
}
}
复制代码
该类很简单,最主要的是调用Downsampler#decode
方法,Downsampler 直译向下采样器,接下来就重点看下该类。
Downsampler
首先来看 Downsampler
对外提供的方法 decode
方法
public Resource decode(InputStream is, int requestedWidth, int requestedHeight,
Options options, DecodeCallbacks callbacks) throws IOException {
Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
+ " mark()");
/* 开始构建 BitmpFactory.Options */
byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
bitmapFactoryOptions.inTempStorage = bytesForOptions;
DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
boolean isHardwareConfigAllowed =
options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);
try {
Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth,
requestedHeight, fixBitmapToRequestedDimensions, callbacks);
return BitmapResource.obtain(result, bitmapPool);
} finally {
releaseOptions(bitmapFactoryOptions);
byteArrayPool.put(bytesForOptions);
}
}
复制代码
该方法首先为 BitmapFactory.Options 设置所需要的参数
-
inTempStorage
Temp storage to use for decoding. Suggest 16K or so. Glide 在这里用的是 64k
-
decodeFormat
解码格式, glide 中的图片主要为两种模式 ARGB_8888, RGB_565
-
fixBitmapToRequestedDimensions
默认为 false(暂时不太理解这个属性的含义,也无法设置成 true)
-
isHardwareConfigAllowed
硬件位图
默认禁用
boolean isHardwareConfigSafe = dataSource == DataSource.RESOURCE_DISK_CACHE || decodeHelper.isScaleOnlyOrNoTransform(); Boolean isHardwareConfigAllowed = options.get(Downsampler.ALLOW_HARDWARE_CONFIG); 复制代码
接下来通过 decodeFromWrappedStream 获取 bitmap,该方法主要逻辑如下:
int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool); //获取原始图片的宽高
int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
calculateScaling(); //设置 inSampleSize 缩放(采样)比例
calculateConfig();
Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
callbacks.onDecodeComplete(bitmapPool, downsampled);
复制代码
我们先来理清这几个size,以 width 为例
- sourceWidth: 即你从网络下载的原始图片的宽
- requestedWidth: 默认为 ImageView 的宽
- targeWidth: 最终生成的 bitmap 的宽
接下来分析 calculateScaling 方法
由于都是计算相关,所以举个栗子,假设图片的sourceWidth 为 1000, targetWidth为 200, sourceHeight为 1200, targetWidth 为 300
final float exactScaleFactor = downsampleStrategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight); //假设向下采样策略为 CenterOutside 实现,则exactScaleFactor 等于 0.25
SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding(sourceWidth,
sourceHeight, targetWidth, targetHeight); //rouding 为 QUALITY
int outWidth = round(exactScaleFactor * sourceWidth); //outWidth = 0.25*1000 + 0.5 = 250
int outHeight = round(exactScaleFactor * sourceHeight); // outHeight = 0.25*1200 + 0.5 = 300
int widthScaleFactor = sourceWidth / outWidth; //widthScaleFactor = 1000/250 = 4
int heightScaleFactor = sourceHeight / outHeight; //heightScalFactor = 1200/300 = 4
int scaleFactor = rounding == SampleSizeRounding.MEMORY //scaleFactor = 4
? Math.max(widthScaleFactor, heightScaleFactor)
: Math.min(widthScaleFactor, heightScaleFactor);
int powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor)); //powerOfTowSampleSize = 4,且只可能是 1,2,4,8,16 ...
if (rounding == SampleSizeRounding.MEMORY
&& powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
}
}
options.inSampleSize = powerOfTwoSampleSize;
// 这里暂时还不太理解,看算法这里的 inTragetDesity 和 inDensity 的比值永远为 1
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
options.inDensity = getDensityMultiplier(adjustedScaleFactor);
}
if (isScaling(options)) {
options.inScaled = true;
} else {
options.inDensity = options.inTargetDensity = 0;
}
复制代码
我们简单看下 CenterOutside
类,代码很简单:
public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth,
int requestedHeight) {
float widthPercentage = requestedWidth / (float) sourceWidth;
float heightPercentage = requestedHeight / (float) sourceHeight;
return Math.max(widthPercentage, heightPercentage);
}
@Override
public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight,
int requestedWidth, int requestedHeight) {
return SampleSizeRounding.QUALITY; // 返回值有 QUALITY 和 MEMORY,其中 MEMORY 相比较 QUALITY 会占用更少内存
}
}
复制代码
接下来通过调用calculateConfig为 options 设置其他属性
if (hardwareConfigState.setHardwareConfigIfAllowed(
targetWidth,
targetHeight,
optionsWithScaling,
format,
isHardwareConfigAllowed,
isExifOrientationRequired)) {
return;
}
// Changing configs can cause skewing on 4.1, see issue #128.
if (format == DecodeFormat.PREFER_ARGB_8888
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888;
return;
}
boolean hasAlpha = false;
try {
hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot determine whether the image has alpha or not from header"
+ ", format " + format, e);
}
}
optionsWithScaling.inPreferredConfig =
hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
if (optionsWithScaling.inPreferredConfig == Config.RGB_565) {
optionsWithScaling.inDither = true;
}
复制代码
最终调用 ecodeStream
方法,该方法通过对 android api BitmapFactory#decodeStream
对图片进行压缩获得了 bitmap 对象
特别注意的是我们在使用 Glide 时加载的网络图片时,默认都是根据 ImageView 的尺寸大小进行了一定比例的,详细的计算过程在上文中也已经提到。但在实际应用中会有希望让用户看到原图场景,这个时候我们可以这样操作
ImgurGlide.with(vh.imageView)
.load(image.link)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) // 硬盘缓存保存原图
.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) // 重载 requestSize,避免 bitmap 被压缩
.into(vh.imageView);
复制代码
Skia 库
在 android 中, BitmapFactory.decodeStream
调用的是 natvie 方法,该函数最终调用的是 skia 库中的encodeStream函数来对图片进行压缩编码。接下来大致介绍一下skia库。
Skia 是一个 c++实现的代码库,在android 中以扩展库的形式存在,目录为external/skia/。总体来说skia是个相对简单的库,在android中提供了基本的画图和简单的编解码功能。另外,skia 同样可以挂接其他第3方编码解码库或者硬件编解码库,例如libpng和libjpeg。在Android中skia就是这么做的,\external\skia\src\images文件夹下面,有几个SkImageDecoder_xxx.cpp文件,他们都是继承自SkImageDecoder.cpp类,并利用第三方库对相应类型文件解码,最后再通过SkTRegistry注册,代码如下所示
static SkTRegistry gDReg(sk_libjpeg_dfactory);
static SkTRegistry gFormatReg(get_format_jpeg);
static SkTRegistry gEReg(sk_libjpeg_efactory);
复制代码
Android编码保存图片就是通过Java层函数——Native层函数——Skia库函数——对应第三方库函数(例如libjpeg),这一层层调用做到的。
最后推荐一个第三方库glide-transformations,可以实现很多图片效果,比如圆角,高斯模糊,黑白。