疑问:
1、图片在内存中的大小跟imageview设置的宽高有没影响?
2、同一张图片放在drawable里面,跟放在drawable-xxhdpi目录里面,加载到内存中大小有什么区别?
3、如何减少Bitmap的内存占用?
4、加载svg图片的内存怎么计算?
结论:Bitmap内存占用跟以下因素有关
1、手机屏幕密度
2、图片资源存放的目录
3、bitmap宽高以及色彩格式
1、加载相同图片,只修改ImageView宽高
测试imageview加载图片内存大小,改变imageview的大小
/***
* 计算ImageView中加载图片的具体尺寸和内存占用大小
* @param imageView
*/
private void calculateBitmapInfo(ImageView imageView) {
Drawable drawable = imageView.getDrawable();
if (drawable != null) {
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Log.d(TAG, "imageView.getWidth() " + imageView.getWidth() +
" bitmap width = " + bitmap.getWidth() +
" bitmap height = " + bitmap.getHeight() +
" memory usage = " + bitmap.getAllocationByteCount());
}
}
final ImageView img = findViewById(R.id.imag1000);
img.post(new Runnable() {
@Override
public void run() {
calculateBitmapInfo(img);
}
});
结果:改变imageview大小,内存占用不变
2021-02-17 01:37:28.709 18883-18883/com.xuanliangdev.firstproject D/MainActivity: imageView.getWidth() 1000 bitmap width = 126 bitmap height = 126 memory usage = 63504
2021-02-17 01:34:56.147 18752-18752/? D/MainActivity: imageView.getWidth() 500 bitmap width = 126 bitmap height = 126 memory usage = 63504
答案:图片在内存中的大小跟imageview设置的宽高没影响。
2.1、加载相同图片,但是图片放在不同的drawable目录
通过如下的方式也可以直接算出一张图片的内存大小占用
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launchern);
Log.d(TAG, " bitmap width = " + bitmap.getWidth()
+ " bitmap height = " + bitmap.getHeight()
+ " memory usage = " + bitmap.getAllocationByteCount());
针对一张192*192的png分别放在drawable、drawable-xhdpi、drawable-xxhdip、drawable-xxxhdpi在密度手机屏幕密度为480的模拟器上测试,结果分别为
drawable
2021-02-17 00:07:12.461 16564-16564/com.xuanliangdev.firstproject D/MainActivity: bitmap width = 576 bitmap height = 576 memory usage = 1327104
drawable-xhdpi
2021-02-17 00:08:33.710 16689-16689/? D/MainActivity: bitmap width = 288 bitmap height = 288 memory usage = 331776
drawable-xxhdip
2021-02-17 00:09:05.434 16820-16820/com.xuanliangdev.firstproject D/MainActivity: bitmap width = 192 bitmap height = 192 memory usage = 147456
drawable-xxxhdpi
2021-02-17 00:09:43.582 16936-16936/com.xuanliangdev.firstproject D/MainActivity: bitmap width = 144 bitmap height = 144 memory usage = 82944
答案:同一张图片放在不同的drawable文件夹,加载到内存中的大小是不一样的。
宽为192的png图片放在drawable-xxhdpi目录,在屏幕密度为480的手机,加载到内存的宽为:width = 192 * (480)/(480) = 192
放在drawable-xxxhdpi目录加载到内存的宽为:width = 192 * (480)/(640) = 144
放在drawable-xhdpi目录加载到内存的宽为:width = 192 * (480)/(320) = 288
放在drawable目录加载到内存的宽为:width = 192 * (480)/(160) = 578
高度计算方法一样。
公式:
1、scale = (float) targetDensity / density;(targetDensity为机型分辨率密度,density为资源所在目录密度,如默认drawable为160,drawable-xxhdpi为480)
2、scaledWidth = width * scale + 0.5f;
2.2、bitmap解码相关源码:
2.2.1、BitmapFactory.decodeResource
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
BitmapFactory.java
/**
* Synonym for {@link #decodeResource(Resources, int, android.graphics.BitmapFactory.Options)}
* with null Options.
*
* @param res The resources object containing the image data
* @param id The resource id of the image data
* @return The decoded bitmap, or null if the image could not be decoded.
*/
public static Bitmap decodeResource(Resources res, int id) {
return decodeResource(res, id, null);
}
public static Bitmap decodeResource(Resources res, int id, Options opts) {
...
final TypedValue value = new TypedValue();
is = res.openRawResource(id, value);
...
bm = decodeResourceStream(res, value, is, null, opts);
...
return bm;
}
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
// TypedValue会取出res资源信息,比如从xxxhdp目录取出的图片资源,value.density为640.
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);
}
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
@Nullable Options opts) {
..
Bitmap bm = null;
..
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts, Options.nativeInBitmap(opts),
..
// 重新设置java层bitmap的scale
setDensityFromOptions(bm, opts);
..
return bm;
}
static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jlong native_asset,
jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) {
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
return doDecode(env, std::make_unique<AssetStreamAdaptor>(asset), padding, options,
inBitmapHandle, colorSpaceHandle);
}
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
jobject padding, jobject options, jlong inBitmapHandle,
jlong colorSpaceHandle) {
...
// Update with options supplied by the client.
if (options != NULL) {
...
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
// targetDensity为手机屏幕密度,density为当前资源目录的密度
scale = (float) targetDensity / density;
}
}
}
// Scale is necessary due to density differences.
if (scale != 1.0f) {
willScale = true;
// 此处对bitmap的宽高进行调整
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
...
// 创建bitmap
// now create the java bitmap
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
// native层回调创建Java的bitmap对象
// called from JNI and Bitmap_Delegate.
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc) {
...
}
// 重新设置java层bitmap的scale
/**
* Set the newly decoded bitmap's density based on the Options.
*/
private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
if (outputBitmap == null || opts == null) return;
final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}
byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled || isNinePatch) {
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}
2.2.2、Bitmap#getAllocationByteCount
/**
* Returns the size of the allocated memory used to store this bitmap's pixels.
*
* <p>This can be larger than the result of {@link #getByteCount()} if a bitmap is reused to
* decode other bitmaps of smaller size, or by manual reconfiguration. See {@link
* #reconfigure(int, int, Config)}, {@link #setWidth(int)}, {@link #setHeight(int)}, {@link
* #setConfig(Bitmap.Config)}, and {@link BitmapFactory.Options#inBitmap
* BitmapFactory.Options.inBitmap}. If a bitmap is not modified in this way, this value will be
* the same as that returned by {@link #getByteCount()}.</p>
*
* <p>This value will not change over the lifetime of a Bitmap.</p>
*
* @see #reconfigure(int, int, Config)
*/
public final int getAllocationByteCount() {
if (mRecycled) {
Log.w(TAG, "Called getAllocationByteCount() on a recycle()'d bitmap! "
+ "This is undefined behavior!");
return 0;
}
return nativeGetAllocationByteCount(mNativePtr);
}
jni/Bitmap.cpp
{ "nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount }
static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) {
LocalScopedBitmap bitmapHandle(bitmapPtr);
return static_cast<jint>(bitmapHandle->getAllocationByteCount());
}
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
return static_cast<jint>(bitmap->rowBytes());
}
size_t rowBytes() const {
if (mBitmap) {
return mBitmap->rowBytes();
}
return mRowBytes;
}
hwui/Bitmap.cpp
size_t Bitmap::getAllocationByteCount() const {
switch (mPixelStorageType) {
case PixelStorageType::Heap:
return mPixelStorage.heap.size;
case PixelStorageType::Ashmem:
return mPixelStorage.ashmem.size;
default:
return rowBytes() * height();
}
}
readpixels.cpp
size_t rowBytes = image->width() * SkColorTypeBytesPerPixel(dstColorType);
参考文档:
https://developer.android.com/training/multiscreen/screendensities?hl=zh-cn
https://juejin.cn/post/6844903428729094157
3.如何正确的加载一张图片而不造成内存浪费
通过前两节知道可以通过减少bitmap的宽高或者色彩格式来降低内存消耗。
1、可以通过在不同的drawable目录内置不同分辨率的图片,注意图片不能只放在默认的drawable中,需要尽量往高分辨率的目录放,由上面的测试结果可以知道,同一张192*192的图片,仅仅放在默认drawable目录比放在drawable-xxhdi(针对匹配的屏幕密度为480的机型,目前市面绝大部分机型都是这个值,小米手机90%机型是440屏幕密度)内存消耗多9倍((480/160)^2)(这里暂时不考虑图片宽高减小造成的图片分辨率变低的问题)。
2、通过代码修改bitmap的inSampleSize值直接等比例缩小图片宽高,从而适应imageview大小,来减少内存消耗,官网文档https://developer.android.com/topic/performance/graphics/load-bitmap建议我们用一些优秀的三方图片加载库来加载图片,也提供了一个方法找出适当的inSampleSize值来适应bitmap的宽高和imageview的宽高。
// 计算出合适的inSampleSize值
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
// 通过计算出的inSampleSize值来调整bitmap的宽高
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
// 加载图片
imageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));