疑问:

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));