作者:彭丑丑
前言
本文将从源码深入剖析 「图片加载」 的过程。
❝
为方便大家理解,源码已作简化处理。
❞
图片资源加载过程
首先我们看下加载图片资源的入口方法:Bi()
// Bi() public static Bitmap decodeResource(Resources res, int id) { return decodeResource(res, id, null); } 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); }
所以,图片资源加载主要分为两步,具体如下图:
步骤1:匹配资源 ID
作用
从资源 id(一个 int 值)定位到具体某一个文件夹下的资源,即「获得 InputStream和TypedValue(即带有文件夹对应的 densityDpi)」。
源码解析
// 步骤说明 public static Bitmap decodeResource(Resources res, int id, Options opts) { // 步骤1:匹配资源 id,打开InputStream -> 关注1 final TypedValue value = new TypedValue(); InputStream is = res.openRawResource(id, value); // 步骤2:解码资源,返回 Bitmap return decodeResourceStream(res, value, is, null, opts); } /** * 关注1:Re() */ InputStream openRawResource(@RawRes int id, TypedValue value) { // 匹配资源 -> 关注2 getValue(id, value, true); // 打开输入流 return mA, value.(), A); } /** * 关注2:getValue() */ void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) { // 查找资源Id & 相关信息存储在 outValue -> 关注3 boolean found = mA(id, 0, outValue, resolveRefs); ... } /** * 关注3:A() */ boolean getResourceValue(@AnyRes int resId, int densityDpi, TypedValue outValue, boolean resolveRefs) { // 从Native文件中查找 -> 关注4 final int cookie = nativeGetResourceValue(mObject, resId, (short) densityDpi, outValue, resolveRefs); if (cookie <= 0) { return false; } return true; } /** * 关注4:A() */ static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid, jshort density, jobject typed_value, jboolean resolve_references) { // ->关注5 return CopyValue(env, cookie, value, ref, flags, &selected_config, typed_value); } /** * 关注5:A() */ static jint CopyValue(JNIEnv* env, ...jobject out_typed_value) { ... if (config != nullptr) { // 将文件夹对应的 density 保存在 TypeValue 中 // 具体资源匹配如下流程图表示 env->SetIntField(out_typed_value, gTy, config->density); } return static_cast<jint>(ApkAssetsCookieToJavaCookie(cookie)); }
具体资源匹配如下流程图表示。
步骤2:解码资源
作用
对图片资源进行解码,并获得图片资源Bitmap。
源码解析
/** * 步骤说明 */ 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 -> 关注2 return decodeResourceStream(res, value, is, null, opts); } /** * 关注1:BiStream() */ public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } // 若未设置 inDensity,则设置为文件夹对应的 densityDpi if == 0 && value != null) { final int density = value.density; if (density == Ty) { o = Di; } else if (density != Ty) { o = density; } } // 若未设置inTargetDensity,则设置为设备的 densityDpi if == 0 && res != null) { o = res.getDisplayMetrics().densityDpi; } // 执行解码 -> 关注2 return decodeStream(is, pad, opts); } /** * 关注2:Bi() */ public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) { // 对于AssetManager输入流(如:/asset、/raw、/drawable) if (is instanceof A) { final long asset = ((A) is).getNativeAsset(); // 核心解码方法 return nativeDecodeAsset(asset, outPadding, opts, O(opts), O(opts)); } else { // 对于其他输入流(如 FileInputStream)-> 关注3 return decodeStreamInternal(is, outPadding, opts); } } /** * 关注3:BiInternal() */ private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) { byte [] tempStorage = null; // 创建一块可复用的中间内存 if (opts != null) tempStorage = o; if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE]; // 核心解码方法 return nativeDecodeStream(is, tempStorage, outPadding, opts,O(opts), O(opts)); }
从上面可以看出,对于不同输入流采用的解码资源方式会不一样(调用不同的native方法):
- 对于AssetManager 输入流(如:/asset、/raw、/drawable):调用nativeDecodeAsset()
- 对于其他输入流(如 FileInputStream):调用nativeDecodeStream()
/** * 对于AssetManager 输入流 * 此处采用的解码方式是:Bi() */ 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); } /** * 对于其他输入流 * 此处采用的解码方式是:Bi() */ 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 ()) { std::unique_ptr<SkStreamRewindable> bufferedStream( SkFrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded())); // 最终执行解码 bitmap = doDecode(env, std::move(bufferedStream), padding, options); } return bitmap; }
从上面可以看出,无论是哪种资源输入流,最终调用的解码方法都是:Bi()
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 Con are always immutable"); return nullObjectReturn("Cannot create mutable hardware bitmap"); } // 步骤4:根据 sampleSize 确定采样后的尺寸(size) SkISize size = codec->getSampledDimensions(sampleSize); int scaledWidth = (); int scaledHeight = (); // 步骤5:确定 java 层 Options 对象 out 字段最终值 Options#outWidth = scaledWidth Options#outHeight = scaledHeight Options#outMimeType = (例如 "image/png") Options#outConfig = (例如 ARGB_8888) Options#outColorSpace =(例如 RGB) // 若inJustDecodeBounds = true,只获取采样后的尺寸(无缩放) // 则直接返回 if (onlyDecodeSize) { return nullptr; } // 步骤6:确定最终缩放到的目标尺寸(先采样,再缩放) 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); } // 步骤7:存在 java 层的 Options#inBitmap,做一些准备工作 android::Bitmap* reuseBitmap = nullptr; if (javaBitmap != NULL) { reuseBitmap = &bitmap::toBitmap(env, javaBitmap); } RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize); // 步骤8:采样解码得到 SkBitmap(注意:只使用了采用后尺寸) const SkImageInfo decodeInfo = SkImageInfo::Make((), (), decodeColorType, alphaType, decodeColorSpace); SkBitmap decodingBitmap; decodingBi(bitmapInfo); decodingBi(decodeAllocator) // 步骤9:执行缩放 SkBitmap outputBitmap; if (willScale) { // 1. 根据是否有可回收的 inBitmap,确定不同的分配器 SkBitmap::Allocator* outputAllocator; if (javaBitmap != nullptr) { outputAllocator = &recyclingAllocator; } else { outputAllocator = &defaultAllocator; } // 2. 复制(注意:使用了缩放后尺寸) SkColorType scaledColorType = decodingBi(); outputBi(scaledWidth, scaledHeight).makeColorType(scaledColorType)); outputBi(outputAllocator) // 3. 利用 Canvas 进行缩放 const float scaleX = scaledWidth / floa()); const float scaleY = scaledHeight / floa()); SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy); canvas.scale(scaleX, scaleY); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); } else { ou(decodingBitmap); } // 返回点1:java 层的 Options#inBitmap if (javaBitmap != nullptr) { return javaBitmap } // 返回点2:硬件位图(from Android 8) if (isHardware) { sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap); return bitmap::createBitmap(env, (), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } // 返回3:普通情况下一般会走这个返回逻辑 return bitmap::createBitmap(env, de(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); }
最后,在这里我就再分享一份大佬亲自收录整理的Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来面试取到一份不错的答卷。
后续也将整理鸿蒙开发的相关资料,希望可以帮助到大家
当然,你也可以拿去查漏补缺,提升自身的竞争力。
如果你有需要的话,只需私信我【进阶】即可获取