Android中ImageDecoder源码分析

1,212 阅读10分钟

一、背景

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“方法为核心,分析图片的解码流程。

我们接下来的分析顺序如下:

  1. ImageDecoder 相关结构分析
  2. 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的一些子类:

image.png 我们看一下 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对象。

和我最初理解有偏差 🙂

  1. 该类支持哪些图片类型:
  • PNG
  • JPEG
  • WEBP
  • GIF
  • HEIF
  1. 使用方式

解码成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对象

image.png 蓝色框部分 各种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的结构和大致的流程了。

ImageDecoderJava层结构简图.png

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());
}

在上述代码中,主要就做了三件事:

  1. 创建ImageDecoder对象
  2. 解码成Bitmap
  3. 将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);
}

image.png

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;
}

四、写在最后

希望大家多提宝贵意见,一起学习,交流,成长。

参考文章

  1. Bitmap之从生到死
  2. Skia图片解析流程与图片编码原理初探