一、背景
Android加载图片资源到内存,并通过Drawable往Canvas上进行绘制。如果图片文件以流的形式加载如内存,则需要对其解码,今天我们通过Android中提供的ImageDecoder工具类,来分析图片解码流程。
在上篇文章Android中Drawable获取过程分析中,我们知道了 Android系统对于Asset中的图片资源的加载,是以字节流的形式加载到内存,然后进行解码。
// file:ResourcesImpl.java
@Nullable
private Drawable decodeImageDrawable(@NonNull AssetInputStream ais,
@NonNull Resources wrapper, @NonNull TypedValue value) {
// 对”输入流“ 进行了一次包装,便于ImageDecoder进行解码
ImageDecoder.Source src = new ImageDecoder.AssetInputStreamSource(ais,
wrapper, value);
...
// 解码的核心调用
return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
}
今天 我们就围绕 "ImageDecoder" 这个工具类,以 ”decodeDrawable“方法为核心,分析图片的解码流程。
我们接下来的分析顺序如下:
- ImageDecoder 相关结构分析
- decodeDrawable 核心逻辑
二、ImageDecoder 相关结构
2.1 ImageDecoder.Source类
在上一节中,我们知道ImageDecoder解码,需要先将 ”输入“ 封装成 Source对象。 那我们首先看一下”Source“,
// file:ImageDecoder.java
// 对于解码流程整个流程来说, 该来确实是 这个流程的 ”源头“;它提供了如下功能:
// 1. 解码器,解码的核心部件
// 2. 解码必要参数: 如 屏幕密度
public static abstract class Source {
private Source() {}
Resources getResources() { return null; }
int getDensity() { return Bitmap.DENSITY_NONE; }
final int computeDstDensity() {
Resources res = getResources();
if (res == null) {
return Bitmap.getDefaultDensity();
}
return res.getDisplayMetrics().densityDpi;
}
// 提供一个解码器,
@NonNull
abstract ImageDecoder createImageDecoder() throws IOException;
};
接下来,我们看一下Source的一些子类:
我们看一下 AssetInputStreamSource、FileSource的源码:
// file:ImageDecoder.java
public static class AssetInputStreamSource extends Source {
public AssetInputStreamSource(@NonNull AssetInputStream ais,
@NonNull Resources res, @NonNull TypedValue value) {
mAssetInputStream = ais;
mResources = res;
if (value.density == TypedValue.DENSITY_DEFAULT) {
mDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (value.density != TypedValue.DENSITY_NONE) {
mDensity = value.density;
} else {
mDensity = Bitmap.DENSITY_NONE;
}
}
private AssetInputStream mAssetInputStream;
private final Resources mResources;
private final int mDensity;
@Override
public Resources getResources() { return mResources; }
@Override
public int getDensity() {
return mDensity;
}
@Override
public ImageDecoder createImageDecoder() throws IOException {
synchronized (this) {
if (mAssetInputStream == null) {
throw new IOException("Cannot reuse AssetInputStreamSource");
}
AssetInputStream ais = mAssetInputStream;
mAssetInputStream = null;
return createFromAsset(ais, this);
}
}
}
private static class FileSource extends Source {
FileSource(@NonNull File file) {
mFile = file;
}
private final File mFile;
@Override
public ImageDecoder createImageDecoder() throws IOException {
return createFromFile(mFile, this);
}
}
从上述的可以看出,不同类型的Source都会创建自己的解码器-ImageDecoder。 至于如何创建ImageDecoder实例,我们稍后再说。
2.2 ImageDecoder
ImageDecoder类的职责是:将解码的图片转换成Drawable或者Bitmap对象。
和我最初理解有偏差 🙂
- 该类支持哪些图片类型:
- PNG
- JPEG
- WEBP
- GIF
- HEIF
- 使用方式
解码成Drawable对象:
ImageDecoder.Source source = ImageDecoder.createSource(file);
Drawable drawable = ImageDecoder.decodeDrawable(source);
解码成Bitmap对象:(以BitmapDrawable的解码为例)
// file:BitmapDrawable.java
public BitmapDrawable(Resources res, String filepath) {
Bitmap bitmap = null;
// 这种 try(){}catch{} 的写法比较值得推荐,能够自动 ”关闭流“
try (FileInputStream stream = new FileInputStream(filepath)) {
// 还是先封装成为Source,然后进行解码
bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(res, stream),
(decoder, info, src) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
} catch (Exception e) {
...
} finally {
// 将Bitmap存储到ConstantState中,疑惑之处,看上一篇文章。
init(new BitmapState(bitmap), res);
...
}
}
上面我们已经对ImageDecoder有了一个功能和使用有了一个初步的认识,接下来,咱们逐步看一下该类有哪些部分组成:
2.2.1 各种Source及对应的ImageDecoder对象
蓝色框部分 各种Source , 红色框部分是 创建ImageDecoder实例
2.2.2 ImageInfo
一个用来描述解码后的图片信息的数据结构。
public static class ImageInfo {
// 图片大小(宽高)
private final Size mSize;
// 解码器对象
private ImageDecoder mDecoder;
private ImageInfo(@NonNull ImageDecoder decoder) {
mSize = new Size(decoder.mWidth, decoder.mHeight);
mDecoder = decoder;
}
...
// 媒体类型
@NonNull
public String getMimeType() {
return mDecoder.getMimeType();
}
// 图片是否是个动画
public boolean isAnimated() {
return mDecoder.mAnimated;
}
// 颜色空间,主要是针对Bitmap的
@Nullable
public ColorSpace getColorSpace() {
return mDecoder.getColorSpace();
}
};
2.2.3 解码相关配置接口-OnHeaderDecodedListener
public static interface OnHeaderDecodedListener {
// 当图片头被解码完成,并且知道总的大小时,才会回调。多数情况下,用来设置decoder的参数。
public void onHeaderDecoded(@NonNull ImageDecoder decoder,
@NonNull ImageInfo info, @NonNull Source source);
};
2.2.4 解码异常处理-DecodeException
public static interface OnPartialImageListener {
// 部分解码成功时,会执行该方法
boolean onPartialImage(@NonNull DecodeException exception);
};
接下来看一下 DecodeException 都定义了哪些Case
public static final class DecodeException extends IOException {
/**
* 数据读取失败
*/
public static final int SOURCE_EXCEPTION = 1;
/**
* 编码的图片不完整
*/
public static final int SOURCE_INCOMPLETE = 2;
/**
* 编码的图片有错误
*/
public static final int SOURCE_MALFORMED_DATA = 3;
...
}
2.2.5 ImageDecoder的成员
//file:ImageDecoder.java
class ImageDecoder {
...
// native的”引用“
private long mNativePtr;
// 图片宽度和高度
private final int mWidth;
private final int mHeight;
// 是否是动画
private final boolean mAnimated;
// 是否是.9
private final boolean mIsNinePatch;
// 图片的期望宽、高,在解码过程中,可以通过OnHeaderDecodedListener 设置
private int mDesiredWidth;
private int mDesiredHeight;
// 像素点的存储位置, 与 ImageDecoder.cpp 要保持一致
private int mAllocator = ALLOCATOR_DEFAULT;
...
private Rect mCropRect;
private Rect mOutPaddingRect;
private Source mSource;
// 对图片做一些的自定义处理
private PostProcessor mPostProcessor;
private OnPartialImageListener mOnPartialImageListener;
...
// 各种创建ImageDecoder的姿势
private static native ImageDecoder nCreate(long asset, Source src) throws IOException;
private static native ImageDecoder nCreate(ByteBuffer buffer, int position,
int limit, Source src) throws IOException;
private static native ImageDecoder nCreate(byte[] data, int offset, int length,
Source src) throws IOException;
private static native ImageDecoder nCreate(InputStream is, byte[] storage,
Source src) throws IOException;
// The fd must be seekable.
private static native ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException;
// 解码图片的方法调用,为啥没有nDecodeDrawable呢? 因为他不够底层,哈哈
private static native Bitmap nDecodeBitmap(long nativePtr,
@NonNull ImageDecoder decoder,
boolean doPostProcess,
int width, int height,
@Nullable Rect cropRect, boolean mutable,
int allocator, boolean unpremulRequired,
boolean conserveMemory, boolean decodeAsAlphaMask,
long desiredColorSpace, boolean extended)
throws IOException;
private static native Size nGetSampledSize(long nativePtr,
int sampleSize);
private static native void nGetPadding(long nativePtr, @NonNull Rect outRect);
private static native void nClose(long nativePtr);
private static native String nGetMimeType(long nativePtr);
private static native ColorSpace nGetColorSpace(long nativePtr);
}
2.2.6 小结
我们通过对ImageDecoder.java的内容进行分析,已经能够弄清java层ImageDecodor的结构和大致的流程了。
ImageDecoder(Java层) 提供了解码的入口(DecodeBitmap)、参数配置(OnHeaderDecodedListener接口和Post)以及像素点存储位置的指定(mAllocator)。
解码的核心工作,是依赖于native层的实现,我们稍后分析。
2.3 Bitmap和Drawable的关联(补充)
在上篇文章Android中Drawable获取过程分析中,我们已经知道了Drawable是什么了。它就是可以能够往Canvas上绘制内容的对象,至于怎么绘制内容 需要交给具体子类进行;
而Bitmap(位图),则是一个能够往Canavas进行绘制的,数据结构。 因此它也能成为一种Drawable, 那就是BitmapDrawable。 对于Bitmap的结构以及与SkiaBitmap的关联关系,我们会在接下来的文章中进行介绍。
总结来说, Bitmap是BitmapDrawable的真正绘制的实体。
三、ImageDecoder解码分析
我们仍然以前面所说的 asset中图片文件加载为例,进行分析其生成Drawable的过程。
// file:ResourcesImpl.java
@Nullable
private Drawable decodeImageDrawable(@NonNull AssetInputStream ais,
@NonNull Resources wrapper, @NonNull TypedValue value) {
// 对”输入流“ 进行了一次包装,便于ImageDecoder进行解码
ImageDecoder.Source src = new ImageDecoder.AssetInputStreamSource(ais,
wrapper, value);
...
// 解码的核心调用,最后的lamada可以传入null。
return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
// 解码文件头之后 之后的callback
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
}
我又把这个代码贴了一下, 我们是先分析Source呢,还是直接开启decodeDrawable的分析呢? 我选择后者, 因为我们在前面已经了解了Source的主要功能了--提供解码器以及一些配置信息。我们现在完全可以先分析decodeDrawable的核心逻辑, 如果有用到Source的方法时,再按需进行分析。
//file:ImageDecoder.java
private static Drawable decodeDrawableImpl(@NonNull Source src,
@Nullable OnHeaderDecodedListener listener) throws IOException {
// 这里上来就通过Souce获取ImageDecoder对象
try (ImageDecoder decoder = src.createImageDecoder()) {
decoder.mSource = src;
// 这里居然回调了listener
decoder.callHeaderDecoded(listener, src);
...
final int srcDensity = decoder.computeDensity(src);
// 对于动画的处理 AnimatedImageDrawable
if (decoder.mAnimated) {
ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
null : decoder;
decoder.checkState(true);
Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
postProcessPtr, decoder.mDesiredWidth,
decoder.mDesiredHeight, decoder.getColorSpacePtr(),
decoder.checkForExtended(), srcDensity,
src.computeDstDensity(), decoder.mCropRect,
decoder.mInputStream, decoder.mAssetFd);
// d has taken ownership of these objects.
decoder.mInputStream = null;
decoder.mAssetFd = null;
return d;
}
// 重中之重, 核心的解码Bitmap的逻辑
Bitmap bm = decoder.decodeBitmapInternal();
// 这是密度比
bm.setDensity(srcDensity);
Resources res = src.getResources();
// 对于.9的处理
byte[] np = bm.getNinePatchChunk();
if (np != null && NinePatch.isNinePatchChunk(np)) {
Rect opticalInsets = new Rect();
bm.getOpticalInsets(opticalInsets);
Rect padding = decoder.mOutPaddingRect;
if (padding == null) {
padding = new Rect();
}
nGetPadding(decoder.mNativePtr, padding);
return new NinePatchDrawable(res, bm, np, padding,
opticalInsets, null);
}
// 直接根据Bitmap和Res封装成了BitmapDrawable了。
return new BitmapDrawable(res, bm);
}
}
private Bitmap decodeBitmapInternal() throws IOException {
...
// native方法直接,进行解码Bitmap
return nDecodeBitmap(mNativePtr, this, mPostProcessor != null,
mDesiredWidth, mDesiredHeight, mCropRect,
mMutable, mAllocator, mUnpremultipliedRequired,
mConserveMemory, mDecodeAsAlphaMask, getColorSpacePtr(),
checkForExtended());
}
在上述代码中,主要就做了三件事:
- 创建ImageDecoder对象
- 解码成Bitmap
- 将Bitmap封装从Drawable
3.1 ImageDecoder对象创建
Source.createImageDecoder()---> createFromAsset(AssetInputStream,Source)
private static ImageDecoder createFromAsset(AssetInputStream ais,
Source source) throws IOException {
ImageDecoder decoder = null;
try {
// 获取到 native层Asset的地址,这个地址是怎么来的,本文不做介绍,在接下来的文章中会提到
//ImageDecoder_nCreateAsset方法。
long asset = ais.getNativeAsset();
// 通过native方法创建了一个java层的ImageDecoder对象,
// 该方法通过jni调用到
decoder = nCreate(asset, source);
} finally {
if (decoder == null) {
IoUtils.closeQuietly(ais);
} else {
decoder.mInputStream = ais;
decoder.mOwnsInputStream = true;
}
}
return decoder;
}
接下来看一下Native层是如何构造ImageDecoder对象的。
// file:ImageDecoder.cpp
static jobject ImageDecoder_nCreateAsset(JNIEnv* env, jobject /*clazz*/, jlong assetPtr,
jobject source) {
Asset* asset = reinterpret_cast<Asset*>(assetPtr);
// 进行一次封装,转换为SkStream对象
std::unique_ptr<SkStream> stream(new AssetStreamAdaptor(asset));
return native_create(env, std::move(stream), source);
}
接下来看一下native_create做了什么事情:
static jobject native_create(JNIEnv* env, std::unique_ptr<SkStream> stream, jobject source) {
...
// 实例化一个Native层ImageDecoder结构。 ImageDecoder(C++)结构中 有
// std::unique_ptr<SkAndroidCodec> mCodec; sk_sp<NinePatchPeeker> mPeeker; 这两个变量
std::unique_ptr<ImageDecoder> decoder(new ImageDecoder);
SkCodec::Result result;
// 根据图片格式选择合适的SkCodec 比如SkPngCodec
auto codec = SkCodec::MakeFromStream(std::move(stream), &result, decoder->mPeeker.get());
...
const bool animated = codec->getFrameCount() > 1;
....
// 进一步封装了Codec , 做到与图片格式无关
decoder->mCodec = SkAndroidCodec::MakeFromCodec(std::move(codec),
SkAndroidCodec::ExifOrientationBehavior::kRespect);
...
// 然后 获取图片的信息,封装成 ImageDecoder java对象。
const auto& info = decoder->mCodec->getInfo();
const int width = info.width();
const int height = info.height();
const bool isNinePatch = decoder->mPeeker->mPatch != nullptr;
return env->NewObject(gImageDecoder_class, gImageDecoder_constructorMethodID,
reinterpret_cast<jlong>(decoder.release()), width, height,
animated, isNinePatch);
}
3.2 ImageDecoder解码过程
private static native Bitmap nDecodeBitmap(long nativePtr,
@NonNull ImageDecoder decoder,
boolean doPostProcess,
int width, int height,
@Nullable Rect cropRect, boolean mutable,
int allocator, boolean unpremulRequired,
boolean conserveMemory, boolean decodeAsAlphaMask,
long desiredColorSpace, boolean extended)
throws IOException;
解码逻辑的native的实现代码(该方法实现了 内存分配、解码、并通过Bitmap工具类封装成为了一个Bitmap java对象):
static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong nativePtr,
jobject jdecoder, jboolean jpostProcess,
jint desiredWidth, jint desiredHeight, jobject jsubset,
jboolean requireMutable, jint allocator,
jboolean requireUnpremul, jboolean preferRamOverQuality,
jboolean asAlphaMask, jlong colorSpaceHandle,
jboolean extended) {
// 重新获取到native层ImageDecoder对象
auto* decoder = reinterpret_cast<ImageDecoder*>(nativePtr);
// 获得Android解码器
SkAndroidCodec* codec = decoder->mCodec.get();
// 设置预期大小
const SkISize desiredSize = SkISize::Make(desiredWidth, desiredHeight);
SkISize decodeSize = desiredSize;
const int sampleSize = codec->computeSampleSize(&decodeSize);
const bool scale = desiredSize != decodeSize;
SkImageInfo decodeInfo = codec->getInfo().makeWH(decodeSize.width(), decodeSize.height());
....
// 分配内存
sk_sp<Bitmap> nativeBitmap;
if (allocator == ImageDecoder::kSharedMemory_Allocator && !scale && !jsubset) {
nativeBitmap = Bitmap::allocateAshmemBitmap(&bm);
} else {
nativeBitmap = Bitmap::allocateHeapBitmap(&bm);
}
...
SkAndroidCodec::AndroidOptions options;
options.fSampleSize = sampleSize;
// 真正的解码操作,关于解码的实现,再后续的文章解析跟进
// 我们这里可以理解为:解码后的像素数据,都存在了bm.getPixels()指向的内存区域内
auto result = codec->getAndroidPixels(decodeInfo, bm.getPixels(), bm.rowBytes(), &options);
...
// 解码失败时的调用
if (onPartialImageError) {
env->CallVoidMethod(jdecoder, gCallback_onPartialImageMethodID, onPartialImageError,
jexception);
if (env->ExceptionCheck()) {
return nullptr;
}
}
jbyteArray ninePatchChunk = nullptr;
jobject ninePatchInsets = nullptr;
// .9文件处理,会在后续的文章中喜欢
// 图片放大处理
// 如果有必要,调用 PostProcessor.java 做一些必要处理
// 设置bitmap的特性
int bitmapCreateFlags = 0x0;
if (!requireUnpremul) {
// premultiplied.
bitmapCreateFlags |= bitmap::kBitmapCreateFlag_Premultiplied;
}
if (requireMutable) {
bitmapCreateFlags |= bitmap::kBitmapCreateFlag_Mutable;
} else {
// 存储在像素点Hardware中
if (isHardware) {
sk_sp<Bitmap> hwBitmap = Bitmap::allocateHardwareBitmap(bm);
if (hwBitmap) {
hwBitmap->setImmutable();
// 调用bitmap::createBitmap 创建一个Java Bitmap对象
return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags,
ninePatchChunk, ninePatchInsets);
}
}
nativeBitmap->setImmutable();
}
// 调用bitmap::createBitmap 创建一个Java Bitmap对象
return bitmap::createBitmap(env, nativeBitmap.release(), bitmapCreateFlags, ninePatchChunk,
ninePatchInsets);
}
3.3 SkiaBitmap到Bitmap(Java)对象包装过程
上一节中,我们我已经了解到解码之后的对象为Bitmap层对象, 但是Android UI体系都是Java的。因此需要有一个Java版Bitmap 与其保持状态同步。 下文中的createBitmap方法正是完成Java版Bitmap的创建和信息同步的。
jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
int density) {
bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
bool fromMalloc = bitmap->pixelStorageType() == PixelStorageType::Heap;
// 做了一层包装
BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
if (!isMutable) {
bitmapWrapper->bitmap().setImmutable();
}
// 实例化了Bitmap对象,并更新了对应的属性值。
//gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap"));
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc);
if (env->ExceptionCheck() != 0) {
ALOGE("*** Uncaught exception returned from Java call!\n");
env->ExceptionDescribe();
}
return obj;
}
四、写在最后
希望大家多提宝贵意见,一起学习,交流,成长。