前言
- 最近我负责了一些相册相关的需求,在完成业务的同时,也希望对图片加载的过程有更深入的认识;
- 在这篇文章里,我将从源码上探讨 图片加载 的过程,文章中引用的核心源码我已经进行了简化与梳理,相信能极大减低你的学习成本。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
相关文章
目录
1. 图片解码选项
public static class Options { public Bitmap inBitmap; public boolean inMutable; public boolean inJustDecodeBounds; public boolean inPremultiplied; public int inDensity; public int inTargetDensity; public int inScreenDensity; // 用不到 public boolean inScaled; public int inSampleSize; public int outWidth; public int outHeight; public Bitmap.Config outConfig; public ColorSpace outColorSpace; } 复制代码
public Options() { inScaled = true; inPremultiplied = true; } 复制代码
2. 图片资源加载过程概述
现在,我们来看加载图片资源的入口方法:
BitmapFactory.java
入口方法 1 public static Bitmap decodeResource(Resources res, int id) { return decodeResource(res, id, null); } 入口方法 2(已简化) public static Bitmap decodeResource(Resources res, int id, Options opts) { 1、匹配资源 id,打开 InputStream final TypedValue value = new TypedValue(); InputStream is = res.openRawResource(id, value); 2、解码资源,返回 Bitmap return decodeResourceStream(res, value, is, null, opts); } 复制代码
可以看到,两个入口方法的区别在于是否传入Options
,在 第 5 节 我会讲到 通过配置Options
来自定义解码,目前我们先当opts == null
。
简化后的decodeResource(...)
非常清晰,无非是分为两个步骤:
- 1、匹配资源,打开 InputStream
- 2、解码资源,返回 Bitmap
3. 步骤一:匹配资源,打开 InputStream
这个步骤主要完成 从资源 id(一个 int 值)定位到具体某一个文件夹下的资源:
ResourcesImpl.java
-> 1、匹配资源 id,打开 InputStream 最终调用到(已简化): InputStream openRawResource(@RawRes int id, TypedValue value) { 1.1 匹配资源 getValue(id, value, true); 1.2 打开输入流 return mAssets.openNonAsset(value.assetCookie, value.string.toString(), AssetManager.ACCESS_STREAMING); } -> 1.1 void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) { 1.1.1 查找资源 id,并将相关信息存储在 outValue boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs); if (!found) { 1.1.2 资源未找到,抛出异常 throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)); } } 复制代码
AssetManager.java
-> 1.1.1 boolean getResourceValue(@AnyRes int resId, int densityDpi, TypedValue outValue, boolean resolveRefs) { final int cookie = nativeGetResourceValue(mObject, resId, (short) densityDpi, outValue, resolveRefs); if (cookie <= 0) { return false; } return true; } 复制代码
static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid, jshort density, jobject typed_value, jboolean resolve_references) { 匹配过程略,见下图... return CopyValue(env, cookie, value, ref, flags, &selected_config, typed_value); } static jint CopyValue(JNIEnv* env, ...jobject out_typed_value) { ... if (config != nullptr) { 关键信息:将文件夹对应的 density 保存在 TypeValue 中 env->SetIntField(out_typed_value, gTypedValueOffsets.mDensity, config->density); } return static_cast<jint>(ApkAssetsCookieToJavaCookie(cookie)); } 复制代码
以上代码已经非常简化了,主要关注以下几点:
- 在 1.1.1 分支,查找资源 id,并将相关信息存储在 outValue,其中比较关键的信息是:将文件夹对应的 densityDpi 保存在 TypeValue 中(这个值在下一节会用到);
- 匹配过程比较冗长,直接看示意图:
引用自 blog.csdn.net/xuaho0907/a… —— 爱吃冰淇淋的羊驼
4. 步骤二:解码资源,返回 Bitmap
在上一步匹配资源中,我们已经获得 InputStream & TypedValue(带有文件夹对应的 densityDpi),这一步我们将对图片资源进行解码,decodeResourceStream()
代码如下:
BitmapFactory.java
-> 2、解码资源,返回 Bitmap public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } 2.1 如果未设置 inDensity,则设置为文件夹对应的 densityDpi if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { 关键点 opts.inDensity = density; } } 2.2 如果未设置 inTargetDensity,则设置为设备的 densityDpi if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } 2.3 执行解码 return decodeStream(is, pad, opts); } -> 2.3 执行解码(已简化) public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) { 2.3.1 AssetManager 输入流(例如:/asset、/raw、/drawable) if (is instanceof AssetManager.AssetInputStream) { final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset(); return nativeDecodeAsset(asset, outPadding, opts, Options.nativeInBitmap(opts), Options.nativeColorSpace(opts)); } else { 2.3.2 其他输入流(例如 FileInputStream) return decodeStreamInternal(is, outPadding, opts); } } -> 2.3.2 private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) { 一块可复用的中间内存 byte [] tempStorage = null; if (opts != null) tempStorage = opts.inTempStorage; if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE]; return nativeDecodeStream(is, tempStorage, outPadding, opts, Options.nativeInBitmap(opts), Options.nativeColorSpace(opts)); } 复制代码
可以看到,在执行decodeStream()
之前有两个比较重要的步骤:
- 2.1 如果未设置 inDensity,则设置为 文件夹对应的 densityDpi
- 2.2 如果未设置 inTargetDensity,则设置为 设备的 densityDpi
到了【2.3 执行解码】,根据是否为 AssetInputStream,调用不同的 native 方法:
- 2.3.1 AssetManager 输入流
(例如:/asset、/raw、/drawable)
,调用nativeDecodeAsset()
- 2.3.2 其他输入流
(例如 FileInputStream)
,调用nativeDecodeStream()
-> 2.3.1 AssetManager 输入流 static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jlong native_asset, jobject padding, jobject options) { Asset* asset = reinterpret_cast<Asset*>(native_asset); 执行解码 return doDecode(env, skstd::make_unique<AssetStreamAdaptor>(asset), padding, options); } -> 2.3.2 其他输入流 static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) { jobject bitmap = NULL; std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage)); if (stream.get()) { std::unique_ptr<SkStreamRewindable> bufferedStream( SkFrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded())); 执行解码 bitmap = doDecode(env, std::move(bufferedStream), padding, options); } return bitmap; } 复制代码
最终都走到doDecode()
,这段代码是图片解码的核心逻辑:
已简化 static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jobject padding, jobject options) { 1、获取 java 层 Options 对象 In 字段值 int sampleSize; 对应于 Options#inSampleSize(默认 1) if (sampleSize <= 0) { sampleSize = 1; } bool onlyDecodeSize; 对应于 Options#inJustDecodeBounds(默认 false) bool isHardware; 对应于 Options#inPreferredConfig(默认 ARGB_8888) bool isMutable; 对应于 Options#inMutable(默认 false) jobject javaBitmap; 对应于 Options#inBitmap(默认 null) boolean inScale; 对应于 Options#inScaled(默认 true) int density; 对应于 Options#inDensity int targetDensity; 对应于 Options#inTargetDensity 2、设置 java 层 Options 对象 out 字段初始值 Options#outWidth = -1 Options#outHeight = -1 Options#outMimeType = 0 Options#outConfig = 0 Options#outColorSpace = 0 3、获得 inDensity / inTargetDensity float scale = 1.0f; if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } mutable 和 hardware 是冲突的 if (isMutable && isHardware) { doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable"); return nullObjectReturn("Cannot create mutable hardware bitmap"); } 4、根据 sampleSize 确定采样后的尺寸(size) SkISize size = codec->getSampledDimensions(sampleSize); int scaledWidth = size.width(); int scaledHeight = size.height(); 5、确定 java 层 Options 对象 out 字段最终值 Options#outWidth = scaledWidth Options#outHeight = scaledHeight Options#outMimeType = (例如 "image/png") Options#outConfig = (例如 ARGB_8888) Options#outColorSpace =(例如 RGB) 6、返回点一:inJustDecodeBounds = true,只获取采样后的尺寸(无缩放) if (onlyDecodeSize) { return nullptr; } 7、确定最终缩放到的目标尺寸(先采样,再缩放) bool willScale = false; if (scale != 1.0f) { willScale = true; scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f); scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f); } 8、存在 java 层的 Options#inBitmap,做一些准备工作 android::Bitmap* reuseBitmap = nullptr; if (javaBitmap != NULL) { reuseBitmap = &bitmap::toBitmap(env, javaBitmap); } RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize); 9、采样解码得到 SkBitmap(注意:只使用了采用后尺寸) const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace); SkBitmap decodingBitmap; decodingBitmap.setInfo(bitmapInfo); decodingBitmap.tryAllocPixels(decodeAllocator) 10、执行缩放 SkBitmap outputBitmap; if (willScale) { 10.1 根据是否有可回收的 inBitmap,确定不同的分配器 SkBitmap::Allocator* outputAllocator; if (javaBitmap != nullptr) { outputAllocator = &recyclingAllocator; } else { outputAllocator = &defaultAllocator; } 10.2 复制(注意:使用了缩放后尺寸) SkColorType scaledColorType = decodingBitmap.colorType(); outputBitmap.setInfo(bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType)); outputBitmap.tryAllocPixels(outputAllocator) 10.3 利用 Canvas 进行缩放 const float scaleX = scaledWidth / float(decodingBitmap.width()); const float scaleY = scaledHeight / float(decodingBitmap.height()); SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy); canvas.scale(scaleX, scaleY); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); } else { outputBitmap.swap(decodingBitmap); } 11、返回点二:返回 java 层的 Options#inBitmap if (javaBitmap != nullptr) { return javaBitmap } 12、返回点三:硬件位图(from Android 8) if (isHardware) { sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap); return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } 13、返回点四:一般方式 return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } 复制代码
提示: 整段源码非常长,阅读的过程是比较痛苦。好处是最终通过源码也发现了不少错误 / 片面的认识,也是收获颇丰。贴心的我当然都帮你整理好了,如果能帮上忙,请务必点赞加关注 ,这真的对我非常重要。