你了解图片加载的过程吗?
Carson带你学习Android
共 20184字,需浏览 41分钟
· 2021-05-12
前言
本文将从源码深入剖析 「图片加载」 的过程。
❝为方便读者理解,源码已作简化处理。
❞
图片资源加载过程
首先我们看下加载图片资源的入口方法:BitmapFactory.decodeResource()
// BitmapFactory.decodeResource()
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:ResourcesImpl.openRawResource()
*/
InputStream openRawResource(@RawRes int id, TypedValue value) {
// 匹配资源 -> 关注2
getValue(id, value, true);
// 打开输入流
return mAssets.openNonAsset(value.assetCookie, value.string.toString(),
AssetManager.ACCESS_STREAMING);
}
/**
* 关注2:getValue()
*/
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) {
// 查找资源Id & 相关信息存储在 outValue -> 关注3
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
...
}
/**
* 关注3:AssetManager.getResourceValue()
*/
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:AssetManager.NativeGetResourceValue()
*/
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:AssetManager.CopyValue()
*/
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));
}
具体资源匹配如下流程图表示。
步骤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:BitmapFactory.decodeResourceStream()
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
// 若未设置 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;
}
}
// 若未设置inTargetDensity,则设置为设备的 densityDpi
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
// 执行解码 -> 关注2
return decodeStream(is, pad, opts);
}
/**
* 关注2:BitmapFactory.decodeStream()
*/
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
// 对于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 {
// 对于其他输入流(如 FileInputStream)-> 关注3
return decodeStreamInternal(is, outPadding, opts);
}
}
/**
* 关注3:BitmapFactory.decodeStreamInternal()
*/
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));
}
从上面可以看出,对于不同输入流采用的解码资源方式会不一样(调用不同的native方法):
对于AssetManager 输入流(如:/asset、/raw、/drawable):调用nativeDecodeAsset() 对于其他输入流(如 FileInputStream):调用nativeDecodeStream()
/**
* 对于AssetManager 输入流
* 此处采用的解码方式是:BitmapFactory.nativeDecodeAsset()
*/
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);
}
/**
* 对于其他输入流
* 此处采用的解码方式是:BitmapFactory.nativeDecodeStream()
*/
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;
}
从上面可以看出,无论是哪种资源输入流,最终调用的解码方法都是:BitmapFactory.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)
// 若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(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace);
SkBitmap decodingBitmap;
decodingBitmap.setInfo(bitmapInfo);
decodingBitmap.tryAllocPixels(decodeAllocator)
// 步骤9:执行缩放
SkBitmap outputBitmap;
if (willScale) {
// 1. 根据是否有可回收的 inBitmap,确定不同的分配器
SkBitmap::Allocator* outputAllocator;
if (javaBitmap != nullptr) {
outputAllocator = &recyclingAllocator;
} else {
outputAllocator = &defaultAllocator;
}
// 2. 复制(注意:使用了缩放后尺寸)
SkColorType scaledColorType = decodingBitmap.colorType();
outputBitmap.setInfo(bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
outputBitmap.tryAllocPixels(outputAllocator)
// 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);
}
// 返回点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, hardwareBitmap.release(), bitmapCreateFlags,
ninePatchChunk, ninePatchInsets, -1);
}
// 返回3:普通情况下一般会走这个返回逻辑
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
「Carson每天带你学习一个Android知识点」,长按扫描关注公众号。同时,期待您精彩文章的投稿:真诚邀请您来分享
最后福利:学习资料赠送
福利:由本人亲自撰写 & 整理的「Android学习方法资料」 数量:10名 参与方式:「点击文章右下角”在看“ -> 回复截图到公众号 即可,我将从中随机抽取」 点击“在看”就能升职 & 加薪水哦!
评论
以数字化转型赋能新型工业化的行业路径:装备制造|以数字化转型赋能新型工业化系列谈(十)
编者按习近平总书记指出,“新时代新征程,以中国式现代化全面推进强国建设、民族复兴伟业,实现新型工业化是关键任务”。为贯彻落实习近平总书记对推进新型工业化作出的重要指示和全国新型工业化推进大会精神,在“赛迪问道数字转型”的研究基础上,赛迪研究院信软所数字化转型课题组聚焦“以数字化转型赋能新型工业化”主
工业互联网世界
0
中华人民共和国和法兰西共和国关于人工智能和全球治理的联合声明
中国邀请法国参加2024世界人工智能大会暨人工智能全球治理高级别会议。冲击万店,汉堡界能跑出下一个蜜雪冰城吗?应法兰西共和国总统埃马纽埃尔·马克龙邀请,中华人民共和国主席习近平于2024年5月5日至7日对法国进行国事访问。在两国建交60周年之际,两国元首重申共同致力于深化2023年4月5日至7日法兰
亿欧网
0
中枪的怎么又是百度?
文 | 阑夕以前说到百度这家公司的槽点,主要还是「上有所好,下必甚焉」的问题,老板想要什么,底下的人就汇报什么,最后把老板骗出去丢人。但是百度公关副总裁接连几天的神操作,又给出了卧龙之外的凤雏答案,那就是老板好像也没做错什么,但经不住管理团队里有人偏要「造」。懈怠的错误和勤奋的愚蠢,就像是屎味的巧克
阑夕
7
接班张勇,46岁的她逆势翻盘
双手改变命运不是口号,而是真实发生的。冲击万店,汉堡界能跑出下一个蜜雪冰城吗?回到29年前,17岁的杨利娟不会想到,挖她跳槽的那家叫“海什么”的火锅店,有朝一日会成为中国最大的连锁火锅品牌。而由于家境困难早早辍学、背了一身债的她,日后会成为管理十几万员工的海底捞首席执行官。听上去,这是一部大女主“升
亿欧网
0
低空经济20人|亿维特任文广:空中出行的梦想实现家
近日,亿维特(南京)航空科技有限公司自主研制的电动垂直起降飞机(eVTOL)ET9原型机成功完成首飞。本期“低空经济20人”系列邀请亿维特创始人、董事长任文广,分享ET9的技术特点及对先进空中交通(AAM)行业的思考和见解。(eVTOL,全称是electric Vertical Take-off a
亿欧网
0
伴学周签 | 什么是阿里人常说的铁军精神 ?
每周一问,共同学习关于茅庐学堂茅庐学堂总结提炼了阿里24年战略落地、组织升级、企业管理和干部培养的实践精华,并结合8年来服务超过300家行业TOP企业的实践打磨,为成长型企业战略落地组织升级提供定制式陪伴咨询服务。想要深入了解茅庐学堂服务欢迎添加学长微信或致电15394275373
茅庐学堂
0
以数字化转型赋能新型工业化的行业路径:原材料 | 以数字化转型赋能新型工业化系列谈(九)
编者按习近平总书记指出,“新时代新征程,以中国式现代化全面推进强国建设、民族复兴伟业,实现新型工业化是关键任务”。为贯彻落实习近平总书记对推进新型工业化作出的重要指示和全国新型工业化推进大会精神,在“赛迪问道数字转型”的研究基础上,赛迪研究院信软所数字化转型课题组聚焦“以数字化转型赋能新型工业化”主
工业互联网世界
0