概述
前置概念-屏幕密度
搞清楚 DisplayMetrics 的两个变量,
density 是显示的逻辑密度,是密度与独立像素单元的比例因子,
densityDpi 是屏幕每英寸对应多少个点
关于DisplayMetrics更多细节点击这里
图片占内存多少的计算原理
找到每个像素占用的字节数*总像素数即可
Android API 有个方便的方法可以获取到占用的内存大小
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
getHeight 就是图片的高度(单位:px)
那么getrowBytes()呢
public final int getrowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mFinalizer.mNativeBitmap);
}
#Bitmap.cpp
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
return static_cast<jint>(bitmap->rowBytes());
}
#SkBitmap.h
/** Return the number of bytes between subsequent rows of the bitmap. */
size_t rowBytes() const { return fRowBytes; }
# SkBitmap.cpp
size_t SkBitmap::ComputeRowBytes(Config c, int width) {
return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
# SkImageInfo.h
static int SkColorTypeBytesPerPixel(SkColorType ct) {
static const uint8_t gSize[] = {
0, // Unknown
1, // Alpha_8
2, // RGB_565
2, // ARGB_4444
4, // RGBA_8888
4, // BGRA_8888
1, // kIndex_8
};
SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
size_mismatch_with_SkColorType_enum);
SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
return gSize[ct];
}
static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
return width * SkColorTypeBytesPerPixel(ct);
}
ARGB_8888(也就是我们最常用的 Bitmap 的格式)的一个像素占用 4byte,rowBytes 实际上就是 4*width bytes.
ARGB_8888 的 Bitmap 占用内存的计算方式为 b i t m a p I n R a m = b i t m a p W i d t h ∗ b i t m a p H e i g h t ∗ 4 b y t e s bitmapInRam = bitmapWidth*bitmapHeight *4 bytes bitmapInRam=bitmapWidth∗bitmapHeight∗4bytes
一张522*686的 PNG 图片,把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,就可以用这个方法获取到。
然而公式计算出来1432368B
density影响内存占用
Bitmap占用空间的大小不止和图片的宽高有关,还与密度因子有关。
读取的是 drawable 目录下面的图片,用的是 decodeResource 方法,该方法本质上就两步:
-
读取原始资源,这个调用了 Resource.openRawResource 方法,这个方法调用完成之后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息;
-
调用 decodeResourceStream 对原始资源进行解码和适配。这个过程实际上就是原始资源的 density 到屏幕 density 的一个映射。
原始资源的 density 其实取决于资源存放的目录(比如 xxhdpi 对应的是480),而屏幕 density 的赋值,
### BitmapFactory.java
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
//实际上,我们这里的opts是null的,所以在这里初始化。
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
//密度等于TypedValue.DENSITY_NONE,那么就没有与资源相关的密度,它不应该被缩放
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//密度等于这个值,那么这个密度应该被视为系统的默认密度值
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;//默认密度160
} else if (density != TypedValue.DENSITY_NONE) {//不等于此值需要缩放
opts.inDensity = density; //这里density的值如果对应资源目录为hdpi的话,就是240
}
}
if (opts.inTargetDensity == 0 && res != null) {
//inTargetDensity就是当前的手机的密度,比如三星s6时就是640
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
我们重点关注两个值 inDensity 和 inTargetDensity,他们与BitmapFactory.cpp文件里面的 density 和 targetDensity相对应
inDensity 就是原始资源的 density,inTargetDensity 就是屏幕的 density。
接着,用到了 nativeDecodeStream 方法,其中最关键的 doDecode 函数的代码:
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
......
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);//对应hdpi的时候,是240
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的为640
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
}
const bool willScale = scale != 1.0f;
......
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
return nullObjectReturn("decoder->decode returned false");
}
//这里这个decodingBitmap就是解码出来的bitmap,大小是图片原始的大小
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
if (willScale) {
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// TODO: avoid copying when scaled size equals decodingBitmap size
SkColorType colorType = 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.
outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
colorType, decodingBitmap.alphaType()));
if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
// If outputBitmap's pixels are newly allocated by Java, there is no need
// to erase to 0, since the pixels were initialized to 0.
if (outputAllocator != &javaAllocator) {
outputBitmap->eraseColor(0);
}
SkPaint paint;
paint.setFilterLevel(SkPaint::kLow_FilterLevel);
SkCanvas canvas(*outputBitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
}
......
}
density 其实是decodingBitmap的 densityDpi ,跟这张图片的放置的目录有关(比如 hdpi 是240,xxhdpi 是480),
targetDensity 实际上是我们加载图片的目标 densityDpi,三星s6为640。sx 和sy 实际上是约等于 scale 的,因为 scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 得到的。我们看到 Canvas 放大了 scale 倍,然后又把读到内存的这张 bitmap 画上去,相当于把这张 bitmap 放大了 scale 倍。
所以回到上面
一张522*686的PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,其中 density 对应 xxhdpi 为480,targetDensity 对应三星s6的密度为640:
522/480 * 640 * 686/480 *640 * 4 = 2546432B
值还是不一样
精度影响内存占用
outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
colorType, decodingBitmap.alphaType()));
最终输出的 outputBitmap 的大小是scaledWidth*scaledHeight,
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
在我们的例子中,
scaledWidth = int( 522 * 640 / 480f + 0.5) = int(696.5) = 696
scaledHeight = int( 686 * 640 / 480f + 0.5) = int(915.16666…) = 915
915 * 696 * 4 = 2547360
Bitmap 在内存当中占用的大小的影响因素
-
色彩格式,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
-
原始文件存放的资源目录
-
目标屏幕的密度
如何优化
知道了原因,那么据此即可优化内存使用。
详情可以查看优化Bitmap内存占用
最后
以上就是风中冬日为你收集整理的Android中一张图片需要占用多少内存的全部内容,希望文章能够帮你解决Android中一张图片需要占用多少内存所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复