图片编解码
在看Fresco对图片解码处理之前,先大致了解一下什么是图片与图片编解码。
图片编码与压缩
图像编码与压缩的本质就是对将要处理的图像源数据按照一定的规则进行变换和组合,从而使得可以用尽可能少的符号来表示尽可能多的信息。这是因为源图像中常常存在各种各样的冗余:空间冗余、时间冗余、信息熵冗余、结构冗余、知识冗余等,这就使得通过编码来进行压缩称为了可能。
根据压缩效果可以分为有损编码和无损编码。有损编码在编码的过程中把不相干的信息都删除了,只能对原图像进行近似的重建;而无损编码的压缩算法中不仅删除了图像数据中的冗余信息,解压缩时能够精确恢复原图像
图片的解码
即把编码的图片还原为原图像。
常见的图片格式
- JPEG
最常见的图片格式,它只支持有损压缩,其压缩算法可以精确控制压缩比,以图像质量换得存储空间。
- PNG
PNG只支持无损压缩,所以它的压缩比是有上限的。相对于 JPEG 和 GIF 来说,它最大的优势在于支持完整的透明通道。
- GIF
只支持256种颜色、透明通道只有1bit、文件压缩比不高。它唯一的优势就是支持多帧动画,凭借这个特性,它得以从 Windows 1.0 时代流行至今,而且仍然大受欢迎。
- WebP
产生的目的是以更高的压缩比替代 JPEG,它支持有损和无损压缩、支持完整的透明通道、也支持多帧动画。
在分析图片的解码前,先理一下前面几节已经分析的图片加载逻辑:
- 首先会直接去缓存读取,查看时候需要加载的图片已经存在 -> BitmapMemoryCacheGetProducer
- 切换到后台线程。 -> ThreadHandoffProducer
- 经过多路复用处理。(同一个请求公用一次加载)。 -> BitmapMemoryCacheKeyMultiplexProducer
- 缓存获取,其实这里主要是wrapper了Consumer,方便以后图片加载之后缓存到缓存。 -> BitmapMemoryCacheProducer
Fresco 图片解码逻辑概览
在Fresco中图片解码对应的Producer是 -> DecodeProducer。首先可以猜想一点的是,按照前面分析的 producer 的执行逻辑,如果在第一次加载图片,那么DecodeProducer
肯定是不会去做图片解码的,图片都没有解码什么呢?它的处理应该是 wrapper consumer,监听图片加载结果,然后做图片的解码。
还是从produceResults
看起:
@Override
public void produceResults(final Consumer<CloseableReference<CloseableImage>> consumer, final ProducerContext producerContext) {
final ImageRequest imageRequest = producerContext.getImageRequest();
ProgressiveDecoder progressiveDecoder;
if (!UriUtil.isNetworkUri(imageRequest.getSourceUri())) {
progressiveDecoder = new LocalImagesProgressiveDecoder(
consumer,
producerContext,
mDecodeCancellationEnabled);
} else {
ProgressiveJpegParser jpegParser = new ProgressiveJpegParser(mByteArrayPool);
progressiveDecoder = new NetworkImagesProgressiveDecoder(
consumer,
producerContext,
jpegParser,
mProgressiveJpegConfig,
mDecodeCancellationEnabled);
}
mInputProducer.produceResults(progressiveDecoder, producerContext);
}
大致逻辑是请求是本地图片的话那就使用LocalImagesProgressiveDecoder
做图片解码,如果是网络图片那就使用NetworkImagesProgressiveDecoder
做图片解码。这里重点关注网络图片的解码,即NetworkImagesProgressiveDecoder
NetworkImagesProgressiveDecoder
这个类继承自 ProgressiveDecoder
, 它对于父类主要的扩展是,增加了实时对jpeg图片解码的能力,提供在只解码了部分jpeg图片时,数据也可以显示的能力:
@Override
protected synchronized boolean updateDecodeJob(EncodedImage encodedImage, @Status int status) {
boolean ret = super.updateDecodeJob(encodedImage, status);
if ((isNotLast(status) || statusHasFlag(status, IS_PARTIAL_RESULT))
&& !statusHasFlag(status, IS_PLACEHOLDER)
&& EncodedImage.isValid(encodedImage)
&& encodedImage.getImageFormat() == DefaultImageFormats.JPEG) {
if (!mProgressiveJpegParser.parseMoreData(encodedImage)) {
return false;
}
//......
}
因此追踪解码功能,这里主要看它的父类ProgressiveDecoder
,先看一下父类的继承结构:
private abstract class ProgressiveDecoder extends DelegatingConsumer<EncodedImage, CloseableReference<CloseableImage>>
看到继承自DelegatingConsumer
,这里就大致明白了,ProgressiveDecoder
主要是用来在图片加载完成后对图片做解码处理,因此先看一下onNewResultImpl
方法:
@Override
public void onNewResultImpl(EncodedImage newResult, @Status int status) {
final boolean isLast = isLast(status);
if (isLast && !EncodedImage.isValid(newResult)) {
handleError(new ExceptionWithNoStacktrace("Encoded image is not valid."));
return;
}
//用来检查解码工作是否完成,如果完成,则返回
if (!updateDecodeJob(newResult, status)) {
return;
}
final boolean isPlaceholder = statusHasFlag(status, IS_PLACEHOLDER);
if (isLast || isPlaceholder || mProducerContext.isIntermediateResultExpected()) {
mJobScheduler.scheduleJob(); //解码
}
}
即如果加载完成图片,则进行图片解码,解码的工作是在指定线程进行的,从mJobScheduler.scheduleJob();
可以看出。其实在解码前还做了一个比较重要的事情,如果设置了网络图片sample或者是本地图片,那么就要设置sample value
if (mDownsampleEnabledForNetwork || !UriUtil.isNetworkUri(request.getSourceUri())) {
encodedImage.setSampleSize(DownsampleUtil.determineSampleSize(request, encodedImage));
}
doDecode(encodedImage, status);
DownsampleUtil.determineSampleSize(request, encodedImage)
方法的目的就是依照下载图片的尺寸和请求的大小或旋转参数来决定一个 sample size。继续看核心doDecode
doDecode
前面看了这么多,终于看到图片解码的地方了。。。 这个方法的核心实现是:
- 对于不完整的jpeg图片不进行解码操作,图片没有请求完成或图片出错也不进行解码操作
- 根据encode图片的 quality 和 decode options做解码操作
image = mImageDecoder.decode(encodedImage, length, quality, mImageDecodeOptions);
mImageDecoder
在这里实际工作的类是DefaultImageDecoder
。看一下其decode
方法:
public CloseableImage decode(EncodedImage encodedImage,int length,QualityInfo qualityInfo,ImageDecodeOptions options) {
ImageFormat imageFormat = encodedImage.getImageFormat();
if (imageFormat == DefaultImageFormats.JPEG) {
return decodeJpeg(encodedImage, length, qualityInfo, options);
} else if (imageFormat == DefaultImageFormats.GIF) {
return decodeGif(encodedImage, length, qualityInfo, options);
} else if (imageFormat == DefaultImageFormats.WEBP_ANIMATED) {
return decodeAnimatedWebp(encodedImage, length, qualityInfo, options);
} else if (imageFormat == ImageFormat.UNKNOWN) {
throw new DecodeException("unknown image format", encodedImage);
}
return decodeStaticImage(encodedImage, options);
}
staticImage 与 JPEG 的解码
对于不同的平台使用的解码算法是不同的,追踪解码实现到: ArtDecoder
。 这两种格式解码的最终实现方法是
CloseableReference<Bitmap> decodeStaticImageFromStream(InputStream inputStream, BitmapFactory.Options options, @Nullable Rect regionToDecode)
不同的就是对于 inputStream
对于StaticImage:
decodeStaticImageFromStream(encodedImage.getInputStream(), options, regionToDecode);
对于jpeg:
InputStream jpegDataStream = encodedImage.getInputStream();
// At this point the InputStream from the encoded image should not be null since in the
// pipeline,this comes from a call stack where this was checked before. Also this method needs
// the InputStream to decode the image so this can't be null.
Preconditions.checkNotNull(jpegDataStream);
if (encodedImage.getSize() > length) {
jpegDataStream = new LimitedInputStream(jpegDataStream, length);
}
if (!isJpegComplete) {
jpegDataStream = new TailAppendingInputStream(jpegDataStream, EOI_TAIL);
}
boolean retryOnFail=options.inPreferredConfig != Bitmap.Config.ARGB_8888;
decodeStaticImageFromStream(jpegDataStream, options, regionToDecode);
这主要是因为jpeg图片支持下载中部分预览。
但是这两种格式的图片的解码最终都会调用 decodeStaticImageFromStream
这个方法内部会去根据 @Nullable Rect regionToDecode
判断是局部解码还是直接整张图片解码,调用相关解码方法:
Bitmap decodedBitmap = null;
ByteBuffer byteBuffer = mDecodeBuffers.acquire();
if (byteBuffer == null) {
byteBuffer = ByteBuffer.allocate(DECODE_BUFFER_SIZE);
}
options.inTempStorage = byteBuffer.array();
//局部解码参数不为null,则进行局部解码
if (regionToDecode != null) {
BitmapRegionDecoder bitmapRegionDecoder = null;
bitmapToReuse.reconfigure(targetWidth, targetHeight, options.inPreferredConfig);
bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, true);
decodedBitmap = bitmapRegionDecoder.decodeRegion(regionToDecode, options);
} catch (IOException e) {
FLog.e(TAG, "Could not decode region %s, decoding full bitmap instead.", regionToDecode);
} finally {
if (bitmapRegionDecoder != null) {
bitmapRegionDecoder.recycle();
}
}
}
//局部解码参数为null或者局部解码失败,则进行整张图片的解码
if (decodedBitmap == null) {
decodedBitmap = BitmapFactory.decodeStream(inputStream, null, options);
}
这两个方法在进行解码是又分别会调用native相关方法去进行解码:
//BitmapRegionDecoder.java
private static native Bitmap nativeDecodeRegion(long lbm,
int start_x, int start_y, int width, int height,
BitmapFactory.Options options);
//BitmapFactory.java
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
GIF 与 WebP 的解码
这两种图片的最终解码是由:GifImage
与WebPImage
实现的,解码的细节其实就是创建这两个对象的实例:
@Override
public AnimatedImage decode(long nativePtr, int sizeInBytes) {
return GifImage.create(nativePtr, sizeInBytes);
}
public static GifImage create(long nativePtr, int sizeInBytes) {
ensure();
Preconditions.checkArgument(nativePtr != 0);
return nativeCreateFromNativeMemory(nativePtr, sizeInBytes);
}
@Override
public AnimatedImage decode(long nativePtr, int sizeInBytes) {
return WebPImage.create(nativePtr, sizeInBytes);
}
public static WebPImage create(long nativePtr, int sizeInBytes) {
ensure();
Preconditions.checkArgument(nativePtr != 0);
return nativeCreateFromNativeMemory(nativePtr, sizeInBytes);
}
但实际上这两种图片经过native方法创建后,Fresco经过一系列处理来支持GIF或webp的显示,以GIF为例:
核心是 : getCloseableImage(options, gifImage, bitmapConfig)
private CloseableImage getCloseableImage(
ImageDecodeOptions options,
AnimatedImage image,
Bitmap.Config bitmapConfig) {
List<CloseableReference<Bitmap>> decodedFrames = null;
CloseableReference<Bitmap> previewBitmap = null;
try {
final int frameForPreview = options.useLastFrameForPreview ? image.getFrameCount() - 1 : 0;
if (options.forceStaticImage) {
return new CloseableStaticBitmap(
createPreviewBitmap(image, bitmapConfig, frameForPreview),
ImmutableQualityInfo.FULL_QUALITY,
0);
}
if (options.decodeAllFrames) {
//拿出一个GIF中所有的帧,每一帧都是一个bigmap
decodedFrames = decodeAllFrames(image, bitmapConfig);
//预览帧
previewBitmap = CloseableReference.cloneOrNull(decodedFrames.get(frameForPreview));
}
if (options.decodePreviewFrame && previewBitmap == null) {
previewBitmap = createPreviewBitmap(image, bitmapConfig, frameForPreview);
}
//使用预览帧和所有帧构造一个 AnimatedImageResult
AnimatedImageResult animatedImageResult = AnimatedImageResult.newBuilder(image)
.setPreviewBitmap(previewBitmap)
.setFrameForPreview(frameForPreview)
.setDecodedFrames(decodedFrames)
.build();
//创建可显示的图片
return new CloseableAnimatedImage(animatedImageResult);
} finally {
CloseableReference.closeSafely(previewBitmap);
CloseableReference.closeSafely(decodedFrames);
}
}
上面整个流程可以总结为下面两张图:


网友评论