复习知识点
相关阅读
OOM系列文章
Android应用OutOfMemory -- 1.OOM机制了解
ANR系列文章
尺寸1200 * 799,体积341.41kb的本地图片加载到内存会占用多少内存呢?
是不是脑海里已经开始计算:
宽 * 高 * 清晰度
=1200 * 799 * 4=3835200约=3.66Mb
下面来看demo测试
private void testBitmap2() {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.img_340k);
Log.e("oom", "getAllocationByteCount..." + bitmap.getAllocationByteCount()
+ "...getByteCount.." + bitmap.getByteCount());
list.add(bitmap);
}
测试机:XiaoMi-8 Android9.0
getAllocationByteCount...3220800...getByteCount..3220800..
哈哈,是不是又不太符合预期? 为什么呢?
咱们就要从Bitmap创建说起了。
介于不同Android版本Bitmap相关代码差异大:
- Android 2.3 (API10) 以及之前 - 像素数据保存在 native heap
- Android 3.0 到 Android 7.1 (API11-26) - 像素数据保存在 java heap
- Android 8.0 以及之后 - 像素数据保存在 native heap
这里我们直接入手Android 8.0来分析
BitmapFactory.decodeResource()
-> BitmapFactory.decodeResourceStream()
->BitmapFactory.decodeStream()
->BitmapFactory.nativeDecodeAsset()
//BitmapFactory.java
public static Bitmap decodeResource(Resources res, int id, Options opts) {
...
bm = decodeResourceStream(res, value, is, null, opts);
...
return bm;
}
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
//这里主要计算density,iTargetDensity,重要参数后面有用
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;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
decodeResourceStream方法主要是计算opts.inTargetDensity和opts.inDensity。
其中TargetDensity指的是我们手机的dpi,而dpi计算方式
屏幕宽平方 * 高平方/屏幕尺寸;
而opts.inDensity指的是你资源放置的目录:
| 目录 | 范围 |
|---|---|
| mdpi | 120dpi-160dpi |
| hdpi | 160dpi~240dpi |
| xhdpi | 240-320dpi |
| xxhdpi | 320-480dpi |
| xxxhdpi | 480-640dpi |
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
...
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}
...
return bm;
}
Native层代码
/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
首先找native注册的gMethod方法
static const JNINativeMethod gMethods[] = {
{ "nativeDecodeAsset",
"(JLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
(void*)nativeDecodeAsset
}
}
static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jlong native_asset,
jobject padding, jobject options) {
Asset* asset = reinterpret_cast<Asset*>(native_asset);
// since we know we'll be done with the asset when we return, we can
// just use a simple wrapper
std::unique_ptr<AssetStreamAdaptor> stream(new AssetStreamAdaptor(asset));
//主要是doDecode方法
return doDecode(env, stream.release(), padding, options);
}
nativeDecodeAsset方法内容很简单,其内容主要是调用了doDecode方法,而构建bitmap对象核心逻辑都在‘doDecode’方法内。 这里说一下无论上层资源来源于:
- File
- Resource
- ByteArray
- Stream
- FileDescriptor 最终都会通过doDecode来创建Bitmap。由于doDecode方法比较长,我们先对其关键步骤进行分解大致为:
- Update with options supplied by the client.(更新opts参数)
- Create the codec.(创建编解码器)
- Handle sampleSize. (跟
BitmapFactory.Options.inSampleSize参数相关) - Set the decode colorType.(设置解码颜色类型)
- Handle scale. (跟
BitmapFactory.Options.inScaled参数相关) - Choose decodeAllocator(选择内存分配器)
- decodingBitmap AllocPixels (解码原始图片分配内存)
- outputBitmap AllocPixels (创建真正输出内存地址,不一定走)
- Use SkAndroidCodec to perform the decode(解码图片)
- Create the java bitmap(创建java层Bitmap对象)
//http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
// This function takes ownership of the input stream. Since the SkAndroidCodec
// will take ownership of the stream, we don't necessarily need to take ownership
// here. This is a precaution - if we were to return before creating the codec,
// we need to make sure that we delete the stream.
std::unique_ptr<SkStreamRewindable> streamDeleter(stream);
// Set default values for the options parameters.
//采样比例
int sampleSize = 1;
//是否只是获取图片的大小
bool onlyDecodeSize = false;
SkColorType prefColorType = kN32_SkColorType;
//硬件加速
bool isHardware = false;
// 是否复用
bool isMutable = false;
//缩放
float scale = 1.0f;
bool requireUnpremultiplied = false;
jobject javaBitmap = NULL;
sk_sp<SkColorSpace> prefColorSpace = nullptr;
// Update with options supplied by the client.
//java层构建好过
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
// Correct a non-positive sampleSize. sampleSize defaults to zero within the
// options object, which is strange.
if (sampleSize <= 0) {
sampleSize = 1;
}
if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
onlyDecodeSize = true;
}
// initialize these, in case we fail later on
env->SetIntField(options, gOptions_widthFieldID, -1);
env->SetIntField(options, gOptions_heightFieldID, -1);
env->SetObjectField(options, gOptions_mimeFieldID, 0);
env->SetObjectField(options, gOptions_outConfigFieldID, 0);
env->SetObjectField(options, gOptions_outColorSpaceFieldID, 0);
jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
jobject jcolorSpace = env->GetObjectField(options, gOptions_colorSpaceFieldID);
prefColorSpace = GraphicsJNI::getNativeColorSpace(env, jcolorSpace);
isHardware = GraphicsJNI::isHardwareConfig(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
//计算缩放比例
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
//获取我们的dpi,之前我们传入xxhdpi下图片的dpi=480
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
//scale=当前设备的dpi/文件目录下的dpi
//scale= opts.inTargetDensity/opts.inDensity=440/480
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
}
...
// Determine the output size.
SkISize size = codec->getSampledDimensions(sampleSize);
//我们图片实际的宽高 300*400
int scaledWidth = size.width();
int scaledHeight = size.height();
bool willScale = false;
// Apply a fine scaling step if necessary.
//如果java设置了insampleSize=2,则实际会缩放四倍
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
willScale = true;
scaledWidth = codec->getInfo().width() / sampleSize;
scaledHeight = codec->getInfo().height() / sampleSize;
}
// Set the decode colorType
SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);
sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(
decodeColorType, prefColorSpace);
// Set the options and return if the client only wants the size.
if (options != NULL) {
jstring mimeType = encodedFormatToString(
env, (SkEncodedImageFormat)codec->getEncodedFormat());
if (env->ExceptionCheck()) {
return nullObjectReturn("OOM in encodedFormatToString()");
}
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
SkColorType outColorType = decodeColorType;
// Scaling can affect the output color type
if (willScale || scale != 1.0f) {
outColorType = colorTypeForScaledOutput(outColorType);
}
jint configID = GraphicsJNI::colorTypeToLegacyBitmapConfig(outColorType);
if (isHardware) {
configID = GraphicsJNI::kHardware_LegacyBitmapConfig;
}
//
jobject config = env->CallStaticObjectMethod(gBitmapConfig_class,
gBitmapConfig_nativeToConfigMethodID, configID);
env->SetObjectField(options, gOptions_outConfigFieldID, config);
env->SetObjectField(options, gOptions_outColorSpaceFieldID,
GraphicsJNI::getColorSpace(env, decodeColorSpace, decodeColorType));
//java中如果调用 options.inJustDecodeBounds=true,则返回null但是会返回原始宽高
if (onlyDecodeSize) {
return nullptr;
}
}
// Scale is necessary due to density differences.
//刚刚我们计算=440/480
if (scale != 1.0f) {
willScale = true;
//由C++primer中知道static_cast<int> 显示转换 会丢掉浮点后的部分
//scaledWidth=(1200*440/480+0.5f)=1100.5f=1100
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
//实际结果=(799*440/480+0.5f)=732.9f=732
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
android::Bitmap* reuseBitmap = nullptr;
unsigned int existingBufferSize = 0;
// 判断是否有复用的 Bitmap 一般是没有的
if (javaBitmap != NULL) {
reuseBitmap = &bitmap::toBitmap(env, javaBitmap);
if (reuseBitmap->isImmutable()) {
ALOGW("Unable to reuse an immutable bitmap as an image decoder target.");
javaBitmap = NULL;
reuseBitmap = nullptr;
} else {
existingBufferSize = bitmap::getBitmapAllocationByteCount(env, javaBitmap);
}
}
HeapAllocator defaultAllocator;
RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
//需要缩放的时候
ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
//根据情况选择不同的内存分配器
SkBitmap::HeapAllocator heapAllocator;
SkBitmap::Allocator* decodeAllocator;
if (javaBitmap != nullptr && willScale) {
// This will allocate pixels using a HeapAllocator, since there will be an extra
// scaling step that copies these pixels into Java memory. This allocator
// also checks that the recycled javaBitmap is large enough.
decodeAllocator = &scaleCheckingAllocator;
} else if (javaBitmap != nullptr) {
decodeAllocator = &recyclingAllocator;
} else if (willScale || isHardware) {
// This will allocate pixels using a HeapAllocator,
// for scale case: there will be an extra scaling step.
// for hardware case: there will be extra swizzling & upload to gralloc step.
decodeAllocator = &heapAllocator;
} else {
decodeAllocator = &defaultAllocator;
}
// Construct a color table for the decode if necessary
sk_sp<SkColorTable> colorTable(nullptr);
SkPMColor* colorPtr = nullptr;
int* colorCount = nullptr;
int maxColors = 256;
SkPMColor colors[256];
if (kIndex_8_SkColorType == decodeColorType) {
colorTable.reset(new SkColorTable(colors, maxColors));
// SkColorTable expects us to initialize all of the colors before creating an
// SkColorTable. However, we are using SkBitmap with an Allocator to allocate
// memory for the decode, so we need to create the SkColorTable before decoding.
// It is safe for SkAndroidCodec to modify the colors because this SkBitmap is
// not being used elsewhere.
colorPtr = const_cast<SkPMColor*>(colorTable->readColors());
colorCount = &maxColors;
}
SkAlphaType alphaType = codec->computeOutputAlphaType(requireUnpremultiplied);
const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(),
decodeColorType, alphaType, decodeColorSpace);
// For wide gamut images, we will leave the color space on the SkBitmap. Otherwise,
// use the default.
SkImageInfo bitmapInfo = decodeInfo;
if (decodeInfo.colorSpace() && decodeInfo.colorSpace()->isSRGB()) {
bitmapInfo = bitmapInfo.makeColorSpace(GraphicsJNI::colorSpaceForType(decodeColorType));
}
if (decodeColorType == kGray_8_SkColorType) {
// The legacy implementation of BitmapFactory used kAlpha8 for
// grayscale images (before kGray8 existed). While the codec
// recognizes kGray8, we need to decode into a kAlpha8 bitmap
// in order to avoid a behavior change.
bitmapInfo =
bitmapInfo.makeColorType(kAlpha_8_SkColorType).makeAlphaType(kPremul_SkAlphaType);
}
SkBitmap decodingBitmap;
//解析原始图片不一定是输出结果 分配内存 可能失败 由于Java heap or native heap
if (!decodingBitmap.setInfo(bitmapInfo) ||
!decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) {
// SkAndroidCodec should recommend a valid SkImageInfo, so setInfo()
// should only only fail if the calculated value for rowBytes is too
// large.
// tryAllocPixels() can fail due to OOM on the Java heap, OOM on the
// native heap, or the recycled javaBitmap being too small to reuse.
return nullptr;
}
...
if (javaBitmap != NULL) {
env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets);
}
}
//真正要返回的图片对象 关键看是否有放缩
//如果是mipmap资源 取决于scale = (float) targetDensity / density; 不为1.0f的时候;
//如果是sd卡或者网络图片取决于 是否有采样 opts.insampleSizes>1的情况
SkBitmap outputBitmap;
if (willScale) {
// This is weird so let me explain: we could use the scale parameter
// directly, but for historical reasons this is how the corresponding
// Dalvik code has always behaved. We simply recreate the behavior here.
// The result is slightly different from simply using scale because of
// the 0.5f rounding bias applied when computing the target image size
//计算转化比例
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// Set the allocator for the outputBitmap.
SkBitmap::Allocator* outputAllocator;
if (javaBitmap != nullptr) {
outputAllocator = &recyclingAllocator;
} else {
outputAllocator = &defaultAllocator;
}
SkColorType scaledColorType = colorTypeForScaledOutput(decodingBitmap.colorType());
// FIXME: If the alphaType is kUnpremul and the image has alpha, the
// colors may not be correct, since Skia does not yet support drawing
// to/from unpremultiplied bitmaps.
//开辟真正使用的bitmap 内存
outputBitmap.setInfo(
bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) {
// This should only fail on OOM. The recyclingAllocator should have
// enough memory since we check this before decoding using the
// scaleCheckingAllocator.
return nullObjectReturn("allocation failed for scaled bitmap");
}
SkPaint paint;
// kSrc_Mode instructs us to overwrite the uninitialized pixels in
// outputBitmap. Otherwise we would blend by default, which is not
// what we want.
paint.setBlendMode(SkBlendMode::kSrc);
paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering
//创建画板
SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
//设置放缩比
canvas.scale(sx, sy);
//绘制
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
outputBitmap.swap(decodingBitmap);
}
...
//创建java层的bitmap对象 像素数据并没有回传。如果是7.0版本代码会将像素数据byte[]交给java层Bitmap对象保存
if (isHardware) {
sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap);
return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,
ninePatchChunk, ninePatchInsets, -1);
}
// now create the java bitmap
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
前面已经对整个方法的关键步骤进行了拆解,这里在提几点:
- 1 .如果java层设置了options.inJustDecodeBounds=true,并不会加载图片到内存,而是直接返回null,从options可以获取到原始宽高。
-
-
关于是否返回原始图片取决于两点,采样率是否insampleSize>1,是否需要放缩scale=1.f;
采样率一般是加载本地或者网络图片时根据目标尺寸去设置,当然其他场景也可设置;
放缩比例则是由加载resource目录下图片时根据资源存放目录及机型来判断。
-
-
- OS3.0-7.0再创建Java层Bitmap对象时会将解码的像素数据byte[]交给java层来保存,而OS8.0及以上则不会。
通过上面的源码解析,可以计算得到:
-
scale= opts.inTargetDensity/opts.inDensity=440/480,则scale!=1.0f
-
scaledWidth=1100,scaledHeight=732
由C++primer中知道static_cast<int> 显示转换 会丢掉浮点后的部分 //scaledWidth=(1200*440/480+0.5f)=1100.5f=1100 scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f); //实际结果=(799*440/480+0.5f)=732.9f=732 scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
那么我们再看下实际情况是否与分析一致,见下图;
我们得到的mWidth * mHeight=1100 * 732;计算实际占用内存为:1100 * 732 * 4 =3220800约=3.07MB,而不是原始宽高 =1200 * 799 * 4=3835200约=3.66Mb。
Bitmap内存如何分配
到这里我们忍不住好奇,Bitmap内存到底是如何分配的呢?
首先是选择 decodeAllocator (见上文提到的 Choose decodeAllocator(选择内存分配器))。
选择 Allocator 时考虑的因素包括:是否复用已有 Bitmap,是否会缩放 Bitmap,是否是 Hardware Bitmap。选择策略总结如下:
| 是否复用已有 Bitmap | 是否会缩放 Bitmap | 是否是 Hardware Bitmap | Allocator类型 |
|---|---|---|---|
| 是 | 是 | - | ScaleCheckingAllocator |
| 是 | 否 | - | RecyclingPixelAllocator |
| 否 | 是 | 是 | SkBitmap::HeapAllocator |
| - | - | - | HeapAllocator (缺省的Allocator) |
BitmapFactory.doDecode()
-> SkBitmap.tryAllocPixels()
-> Allocator.allocPixelRef()
代码如下
从doDecode方法中单独摘取出来
...
SkBitmap::HeapAllocator heapAllocator;
SkBitmap::Allocator* decodeAllocator;
if (javaBitmap != nullptr && willScale) {
// This will allocate pixels using a HeapAllocator, since there will be an extra
// scaling step that copies these pixels into Java memory. This allocator
// also checks that the recycled javaBitmap is large enough.
decodeAllocator = &scaleCheckingAllocator;
} else if (javaBitmap != nullptr) {
decodeAllocator = &recyclingAllocator;
} else if (willScale || isHardware) {
// This will allocate pixels using a HeapAllocator,
// for scale case: there will be an extra scaling step.
// for hardware case: there will be extra swizzling & upload to gralloc step.
decodeAllocator = &heapAllocator;
} else {
decodeAllocator = &defaultAllocator;
}
...
// http://androidxref.com/8.0.0_r4/xref/external/skia/src/core/SkBitmap.cpp
bool SkBitmap::tryAllocPixels(Allocator* allocator, SkColorTable* ctable) {
HeapAllocator stdalloc;
if (nullptr == allocator) {
allocator = &stdalloc;
}
return allocator->allocPixelRef(this, ctable);
}
Allocator 的类型有四种,我们只看其中的两种。
1. 先看 SkBitmap::HeapAllocator 作为 decodeAllocator 进行内存分配的流程。
- SkBitmap::tryAllocPixels
- SkBitmap::HeapAllocator::allocPixelRef
- SkMallocPixelRef::NewAllocate
- [SkMemory_malloc::sk_malloc_flags](/SkMemory_malloc
核心是最终会调用 malloc() 分配指定大小的内存
//http://androidxref.com/8.0.0_r4/xref/external/skia/src/core/SkBitmap.cpp
bool SkBitmap::tryAllocPixels(Allocator* allocator, SkColorTable* ctable) {
HeapAllocator stdalloc;
if (nullptr == allocator) {
allocator = &stdalloc;
}
return allocator->allocPixelRef(this, ctable);
}
//http://androidxref.com/8.0.0_r4/xref/external/skia/src/core/SkBitmap.cpp
/** We explicitly use the same allocator for our pixels that SkMask does,
so that we can freely assign memory allocated by one class to the other.
*/
bool SkBitmap::HeapAllocator::allocPixelRef(SkBitmap* dst,
SkColorTable* ctable) {
const SkImageInfo info = dst->info();
if (kUnknown_SkColorType == info.colorType()) {
// SkDebugf("unsupported config for info %d\n", dst->config());
return false;
}
sk_sp<SkPixelRef> pr(SkMallocPixelRef::NewAllocate(info, dst->rowBytes(), ctable));
if (!pr) {
return false;
}
dst->setPixelRef(std::move(pr), 0, 0);
// since we're already allocated, we lockPixels right away
dst->lockPixels();
return true;
}
//http://androidxref.com/8.0.0_r4/xref/external/skia/src/core/SkMallocPixelRef.cpp
SkMallocPixelRef* SkMallocPixelRef::NewAllocate(const SkImageInfo& info,
size_t rowBytes,
SkColorTable* ctable) {
auto sk_malloc_nothrow = [](size_t size) { return sk_malloc_flags(size, 0); };
return NewUsing(sk_malloc_nothrow, info, rowBytes, ctable);
}
//http://androidxref.com/8.0.0_r4/xref/external/skia/src/ports/SkMemory_malloc.cpp
void* sk_malloc_flags(size_t size, unsigned flags) {
void* p = malloc(size);
if (flags & SK_MALLOC_THROW) {
return throw_on_failure(size, p);
} else {
return p;
}
}
2.再来看 HeapAllocator 作为 decodeAllocator 进行内存分配的流程。
//http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Graphics.cpp
bool HeapAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
mStorage = android::Bitmap::allocateHeapBitmap(bitmap, ctable);
return !!mStorage;
}
//http://androidxref.com/8.0.0_r4/xref/frameworks/base/libs/hwui/hwui/Bitmap.cpp
static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,
SkColorTable* ctable) {
void* addr = calloc(size, 1);
if (!addr) {
return nullptr;
}
return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
}
对于 RecyclingPixelAllocator 和 ScaleCheckingAllocator 的情况类似,还是由 malloc()/ calloc() 分配内存。这里不做继续分析。
Bitmap整体结构
最后来梳理下Bitmap整体结构
-
App 中这些图片实际上是 BitmapDrawable
-
BitmapDrawable 是对 Bitmap 的包装
-
Bitmap 是对 SkBitmap 的包装。具体说来, Bitmap 的具体实现包括 Java 层和 JNI 层, JNI 层依赖 Skia。
-
SkBitmap 可简单理解为内存中的一个字节数组
参考文章: