Android性能优化之Bitmap

Bitmap 简介

Bitmap 在 Android 开发中作为图片信息的载体,比如在列表、Banner、Splash 还是其他界面,图片都是在内存占用中比例比较大的,没有妥善处理很容易导致 OOM(Out Of Memory)和因内存不足频繁 GC 导致 UI 卡顿问题。所以在项目开发中要根据应用场景处理好 Bitmap,减少图片对内存的占用。

Bitmap 的创建

Bitmap 的内存计算是通过图片分辨率和像素点的数据格式计算的,为了验证这个结论,下面通过源码验证这个说法。

在 Java 中创建 Bitmap 的方式是通过 Bitmap.createBitmap():

public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
        @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
    ...

    Bitmap bm;
    if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
        bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);
    } else {
        ...
        bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
                d50.getTransform(), parameters);
    }

    ...
    return bm;
}

private static native Bitmap nativeCreate(int[] colors, int offset,
                                          int stride, int width, int height,
                                          int nativeConfig, boolean mutable,
                                          @Nullable @Size(9) float[] xyzD50,
                                          @Nullable ColorSpace.Rgb.TransferParameters p);

可以发现,Bitmap.createBitmap() 最终都是调用的 nativeCreate() 创建一个 Bitmap,Java 层的 Bitmap 并没有对内存有任何操作,所以 Java 层的 Bitmap 实际上只是一个中间角色,Bitmap 在 native 创建即实际是在 native 分配的内存,而不是在 Java 分配内存。

继续往下跟踪 native 如何创建 Bitmap 并分配内存:

系统源码
frameworks/base/core/jni/android/graphics/Bitmap.cpp

// 动态注册
static const JNINativeMethod gBitmapMethods[] = {
    {   "nativeCreate",             "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;",
        (void*)Bitmap_creator },
};

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jfloatArray xyzD50, jobject transferParameters) {
    ...
    SKBitmap bitmap;
    sk_sp<SkColorSpace> colorSpace;
    
    // 传入 width、height、colorType 作为创建 Bitmap 的信息
    // 在 native 中 Bitmap 实际上是 SkBitmap
    bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphatype, colorSpace))

    // 申请内存
    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap, NULL);
    if (!nativeBitmap) {
        return NULL;
    }

    return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
}

jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
    ...
    BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
    // 会调用到 Java 的 Bitmap 构造函数
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
            isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);

    ...
    return obj;
}

Bitmap.java

Bitmap(long nativeBitmap, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {}

根据 native 的源码可以验证 Bitmap 内存大小就是通过图片分辨率和像素点的数据格式计算的,和图片保存为 png 还是 jpg 格式无关;并且 native 创建的 SKBitmap 才是实际的 Bitmap,通过它操作 Bitmap 的内存(更具体说是一个内存指针,对应 Java 层构造函数传入的 nativeBitmap 参数)。

那 Java 的 Bitmap 不是实际可操作的内存,那为什么我们通过 Bitmap 里面的方法调用就能处理了?这其实是因为 在创建 Bitmap 时在构造函数保存了操作 native bitmap 的指针,通过这个指针就可以找到对应的那块内存:

public final class Bitmap implements Parcelable {
    // Convenience for JNI access
    private final long mNativePtr;

    // called from JNI
    Bitmap(long nativeBitmap, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
        ...
        mNativePtr = nativeBitmap; // native 的指针
    }

    // 要操作 native 那块内存,就需要传入指针,第一个参数都是指针
    private static native Bitmap nativeCopy(long nativeSrcBitmap, int nativeConfig,
                                            boolean isMutable);
    private static native Bitmap nativeCopyAshmem(long nativeSrcBitmap);
    private static native Bitmap nativeCopyAshmemConfig(long nativeSrcBitmap, int nativeConfig);
    private static native long nativeGetNativeFinalizer();
    private static native boolean nativeRecycle(long nativeBitmap);
    private static native void nativeReconfigure(long nativeBitmap, int width, int height,
                                                 int config, boolean isPremultiplied);
    ... 
}

通过 native 指针就能找到 Bitmap 所在的内存区域,我们也用一个 demo 验证一下:

public class ImageHandler {
    static {
        System.loadLibrary("native-lib");
    }
    
    public native int updateFrame(Bitmap bitmap);
}

public class MainActivity extends AppCompatActivity {
    private ImageView imageView;
    private final ImageHandler imageHandler = new ImageHandler();
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = findViewById(R.id.image);
        
        findViewById(R.id.button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                loadBitmap();
            }
        });
    }   

    public void loadBitmap() {
        Bitmap bitmap = Bitmap.createBitmap(600, 800, Bitmap.Config.ARGB_8888);
        imageView.setImageBitmap(bitmap);
        imageHandler.updateFrame(bitmap);
    }
}

native-lib.cpp

// 要获取 bitmap 需要导入 android 的头文件
#include <android/bitmap.h>
#include <android/log.h>
#define LOG_TAG "test"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_demo_ImageHandler_updateFrame(JNIEnv *env, jobject imageHandler thiz, jobject bitmap) {
    // 获取 bitmap 信息,例如宽高、格式等
    AndroidBitmapInfo info;
    AndroidBitmap_getInfo(env, bitmap, &info);
    int width = info.width;
    int height = info.height;
    LOGE("width = %d, height = %d", width, height);

    // 获取图片像素信息,是一个二维数组
    // 需要在 cmake 的 target_link_libraries 加上 jnigraphics 才能使用
    // 它会去找 libjnigraphics.so
    int *pixels = NULL;
    int *px = pixels;
    int *line;
    AndroidBitmap_lockPixels(env, bitmap, reinterpret_cast<void **>(&pixels));
    // 一行行渲染 bitmap 的内存区域
    for (int y = 0; y < height; y++) {
        line = px;
        for (int x = 0; x < width; x++) {
            // 如果用 argb 应该是 0xFFFF0000
            // 但显示器的颜色排列是 abgr,所以要显示为红色应该是 0xFF0000FF
            line[x] = 0xFF0000FF; 
        }
        // stride 是一行像素的字节数
        // 假设分辨率是 1920*1080,用 ARGB_8888 格式一个像素是 4 个字节
        // 那么一行像素的字节数是 1080*4,因为这里要拿的一行的像素,所以要除以 4
        px = px + info.stride / 4; 
        // px = px + width;
    }
    AndroidBitmap_unlockPixels(env, bitmap);
    return 1;
}

上面的 demo 是将创建的 Bitmap 直接操作内存,将所有像素点修改为红色。

可以用 Memory Profiler 看下 Bitmap 是否就是由 native 分配的内存:


image.png

image.png

上图的 native 内存大小从 5.8MB 上升到 7.9MB,加载的 Bitmap 大小是 600x800 使用 ARGB_8888(占用 4 个字节),即加载的内存大小粗略计算是 (600 * 800 * 4) / 1024 / 1024 = 1.8MB,5.8MB + 1.8MB = 7.6MB,已经比较接近。

不同系统版本 Bitmap 的内存分配策略

当我们做内存优化时,Bitmap 是首要处理的方向,相比在小的方面十几二十几k留意代码细节创建对象,从 Bitmap 入手可以非常显著的降低内存。可能项目中一个只需要 100x100 大小的 ImageView,所使用的切图背景却是 320x400 分辨率的图片,如果因为这个图片多占了 1M 的内存,找到这个图片对其缩放裁剪后就能降低将近 1M 内存。

但在着手从 Bitmap 做内存优化前,因为系统的不断的更新迭代优化,Bitmap 在不同的 Android 系统版本的内存分配策略也有所不同,这是我们需要了解的:

系统版本 Bitmap 内存分配策略
Android 3.0 之前 Bitmap 对象存放在 Java Heap,而像素数据是存放在 native 内存中。缺点:如果不手动调用 bitmap.recycle(),Bitmap native 内存的回收完全依赖 finalize() 回调,但是回调时机是不可控的
Android 3.0~7.0 Bitmap 对象和像素数据统一放到 Java Heap,即使不调用 bitmap.recycle(),Bitmap 像素也会随着对象一起被回收。缺点:1、Bitmap 全部放在 Java Heap,Bitmap 是内存消耗的大户,而 Max Heap 一般限制为 256、512MB,Bitmap 过大过多容易导致 OOM。2、容易引起大量 GC,没有充分利用系统的可用内存
Android 8.0 及以后 1、使用了能够辅助回收 native 内存的 NativeAllocationRegistry 实现将像素数据放到 native 内存,并且可以和 Bitmap 对象一起快速释放,在 GC 的时候还可以考虑到这些 Bitmap 内存以防止被滥用。2、新增硬件位图 Hardware Bitmap 解决图片内存占用过多和图像绘制效率过慢问题

Bitmap 内存占用计算

在电脑查看的图片大小和运行内存大小区别


image.png

面的图片显示图片分辨率为 3997x2989,大小为 1.36MB,那这张图片在内存的大小也是 1.36MB 吗?

其实,我们在电脑上看到的 png 格式或者 jpg 格式的图片,png/jpg 只是这张图片的容器,它们是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示,以此达到压缩目的,减少图片文件大小(注意,这里的大小说的是磁盘大小,不是内存大小!)

我们通过代码将图片加载进内存时,会先解析图片文件本身的数据格式,然后还原为 Bitmap,也就是 Bitmap 的大小取决于图片分辨率和像素点的数据格式两者了。

所以,一张 png 或者 jpg 格式的图片大小,跟这张图片加载进内存所占用的大小完全是两回事。

图片占用内存计算

一个 Bitmap 要怎样计算它所占用的内存大小?

刚才我们了解到,Bitmap 的大小取决于像素点的数据格式以及分辨率。Bitmap 用来处理位图,每一张图片的每个像素点都会被读取,每个像素点的大小决定了 Bitmap 的内存大小。计算公式为:

像素总数量(宽x高) * 每个像素占用的字节大小 = Bitmap 占用的内存字节大小

像素总数量(宽x高) :表示的是图片分辨率,比如 1920x1080 的图片分辨率像素总数量为 1920*1080=2073600。

每个像素的字节大小:像素字节大小由 Bitmap 的一个可配置参数 Bitmap.Config 决定,Android 系统默认使用 ARGB_8888 加载 Bitmap:

Config 字节大小byte 说明
ALPHA_8 1 单一透明度
RGB_565 2 无透明度图
ARGB_4444 2 低质量
ARGB_8888 4 默认颜色配置,高质量

所以根据上面的分析,一张分辨率为 1920x1080,大小为 230KB 的 jpg 图片,放在手机的 SD 卡中,通过 BitmapFactory.decodeFile() 加载占用的内存大小为:

粗略计算:
1920x1080 * 4byte(ARGB_8888) = 2073600 * 4byte = 8294400byte / 1024 / 1024 ≈ 7.9MB

加载这张图片使用了大约 8MB 内存,内存占用开销很大。

Bitmap 内存优化

单从图片本身优化考虑有三个方向:

降低分辨率

减少每个像素点占用的字节大小

复用 Bitmap 内存区域

其中,降低分辨率主要是使用 inSampleSize 调整缩放比例;减少每个像素点占用的字节大小主要是使用 inPreferredConfig 指定像素点的数据格式;复用 Bitmap 内存区域是使用 inBitmap 设置要复用的 Bitmap。

inJustDecodeBounds 获取 Bitmap 信息
在实际的获取 Bitmap 之前,为了获取我们处理的 Bitmap 信息,需要将 inJustDecodeBounds 设置为 true,在这之下获取和处理 Bitmap 信息后,再重新将 inJustDecodeBounds 设置为 false。

BitmapFactory.Options options = new BitmapFactory.Options();
// 开始获取处理 Bitmap 信息
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options); // 该句代码获取的 Bitmap 为 null
options.inPreferredConfig = config;
options.inSampleSize = 2;
options.inJustDecodeBounds = false;
// 结束获取处理 Bitmap 信息,实际获取 Bitmap 分配内存
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);

当 inJustDecodeBounds 设置为 true 时,当我们调用 BitmapFactory.decodeXxx() 时并不会发生内存产生分配,具体可以查看 native 源码:

frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

jfieldID gOptions_justBoundsFieldID;

int register_android_graphics_BitmapFactory(JNIEnv* env) {
    gOptions_justBoundsFieldID = GetFieldIDOrDie(env, options_class, "inJustDecodeBounds", "Z");    
}

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    ...
    bool onlyDecodeSize = false;
    ...
    if (options != NULL) {
        ...
        // inJustDecodeBounds 设置为 true
        if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
            onlyDecodeSize = true;
        }   
        ... 
    }

    // Set the options and return if the client only wants the size.
    if (options != NULL) {
        // 获取 Bitmap 信息提供到 Java 层的 Bitmap 对应字段
        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));

        if (onlyDecodeSize) {
            return nullptr; // inJustDecodeBounds=true,不创建 Bitmap 即不分配产生内存
        }
    }
    ...
}

inSampleSize 调整缩放比例

inSampleSize 指的是缩放比例,一般取值都为 2 的幂次。

Bitmap originBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
if (originBitmap != null) {
    Log.i(TAG, "origin bitmap byte: " + Formatter.formatFileSize(this, originBitmap.getAllocationByteCount()));
}

Bitmap argb8888 = scaleBitmap(file, Bitmap.Config.ARGB_8888);
if (argb8888 != null) {
    Log.i(TAG, "argb8888 bitmap byte:" + Formatter.formatFileSize(this, argb8888.getAllocationByteCount()));
}

Bitmap rgb565 = scaleBitmap(file, Bitmap.Config.RGB_565);
if (rgb565 != null) {
    Log.i(TAG, "rgb565 bitmap byte:" + Formatter.formatFileSize(this, rgb565.getAllocationByteCount()));
}

private Bitmap scaleBitmap(File file, Bitmap.Config config) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(file.getAbsolutePath(), options);
    options.inPreferredConfig = config;
    options.inSampleSize = 1;
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
}

// 未设置inSampleSize或inSampleSize=1:
origin bitmap byte: 9.33MB
argb8888 bitmap byte: 9.33MB
rgb565 bitmap byte: 4.67MB

// 设置inSampleSize=2:
origin bitmap byte: 9.33MB
argb8888 bitmap byte: 2.33MB
rgb565 bitmap byte: 1.17MB

可以发现,一张分辨率为 1080x2160 像素点数据格式 ARGB_8888 的图片,当设置 inSampleSize = 2 时,分辨率将会被缩小为 1080/2 x 2160/2= 540 x 1080,像素数和内存占用都被缩小为原来的 1/4。

在实际项目开发中,一般我们会动态的根据需要展示的 ImageView 的大小动态修改 inSampleSize 的值。分辨率为 1080x2160 图片,磁盘大小为 1.08MB,ImageView 宽高都为 200dp,计算占用内存大小:

Bitmap originBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
if (originBitmap != null) {
    Log.i(TAG, "origin bitmap byte: " + Formatter.formatFileSize(this, originBitmap.getAllocationByteCount()));
}

Bitmap argb8888 = scaleBitmap(file, Bitmap.Config.ARGB_8888, imageView.getWidth(), imageView.getHeight());
if (argb8888 != null) {
    Log.i(TAG, "argb8888 bitmap byte:" + Formatter.formatFileSize(this, argb8888.getAllocationByteCount()));
}

Bitmap rgb565 = scaleBitmap(file, Bitmap.Config.RGB_565, imageView.getWidth(), imageView.getHeight());
if (rgb565 != null) {
    Log.i(TAG, "rgb565 bitmap byte:" + Formatter.formatFileSize(this, rgb565.getAllocationByteCount()));
}

private Bitmap scaleBitmap(File, file, Bitmap.Config config, int reqWidth, int reqHeight) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(file.getAbsolutePath(), options);
    options.inPreferredConfig = config;
    options.inSampleSize = calculateSampleSize2(options, reqWidth, reqHeight);
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
}

private int calculateSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // 图片原始宽高
    int width = options.outWidth;
    int height = options.outHeight;

    // 默认不缩放
    int inSampleSize = 1; 
    // 当原始图片宽高大于控件所需宽或高才进行缩放
    if (width > reqWidth || height > reqHeight) {
        // 取宽度比与高度比的最大值
        int widthRound = Math.round(width * 1f / reqWidth);
        int heightRound = Math.round(height * 1f / reqHeight);

        inSampleSize = Math.max(widthRound, heightRound);
    }

    return inSampleSize;
}

运行结果:
inSampleSize=4
origin bitmap byte: 9.33MB
argb8888 bitmap byte: 583kB
rgb565 bitmap byte: 292kB

inPreferredConfig 选择合适的像素点数据格式

Android 系统默认使用 ARGB_8888 加载图片 Bitmap,占用 4 个字节大小。其他像素点数据格式如下表所示:

Config 字节大小(byte) 说明 名字的含义
ALPHA_8 1 单一透明度 只有 Alpha 颜色通道占用 8 位即 1 个字节
RGB_565 2 无透明度图 RGB 是三个颜色通道 Red、Green、Blue,Red、Blue 通道占用 5 位,Green 通道占用 6 位,总共占用 16 位即 2 个字节
ARGB_4444 2 低质量 ARGB 是四个颜色通道 Alpha、Red、Green、Blue,每个通道占用 4 位,总共占用 16 位即 2 个字节
ARGB_8888 4 默认颜色配置,高质量 ARGB 是四个颜色通道 Alpha、Red、Green、Blue,每个通道占用 8 位,总共占用 32 位即 4 个字节

其中,ARGB_4444 已被标记为废弃,API 19 以上的机型将默认使用 ARGB_8888:


image.png

我们分别使用不同的像素点数据格式计算一张图片占用的内存大小,顺便验证为什么 ARGB_4444 会被废弃。

使用 vivo x20 Android 8 系统,图片分辨率为 1080x2160,大小为 1.08MB 的 jpg 图片放在外部存储:

Bitmap originBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
if (originBitmap != null) {
    Log.i(TAG, "origin bitmap byte: " + Formatter.formatFileSize(this, originBitmap.getAllocationByteCount()));
}

Bitmap argb8888 = createBitmap(file, Bitmap.Config.ARGB_8888);
if (argb8888 != null) {
    Log.i(TAG, "argb8888 bitmap byte: " +  Formatter.formatFileSize(this, argb8888.getAllocationByteCount()));
}

Bitmap argb4444 = createBitmap(file, Bitmap.Config.ARGB_4444);
if (argb4444 != null) {
    Log.i(TAG, "argb4444 bitmap byte: " +  Formatter.formatFileSize(this, argb4444.getAllocationByteCount()));
}

Bitmap rgb565= createBitmap(file, Bitmap.Config.RGB_565);
if (rgb565 != null) {
    Log.i(TAG, "rgb565 bitmap byte: " + Formatter.formatFileSize(this, rgb565.getAllocationByteCount()));
}
        
private Bitmap createBitmap(File file, Bitmap.Config config) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(file.getAbsolutePath(), options);
    options.inPreferredConfig = config;
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
}

运行结果:
origin bitmap byte: 9.33MB
argb8888 bitmap byte: 9.33MB
argb4444 bitmap byte: 9.33MB
rgb565 bitmap byte: 4.67MB

或许你会有疑问:为什么 ARGB_4444 和 ARGB_8888 结果是相同的?

上面其实也有注释解释,在 Android 4.4 开始如果使用的 ARGB_4444,将会被直接替换为 ARGB_8888 数据格式,从 Bitmap.createBitmap() 的 native 源码也可以证实:

frameworks/base/core/jni/android/graphics/Bitmap.cpp

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jfloatArray xyzD50, jobject transferParameters) {
    ...
    // ARGB_4444 is a deprecated format, convert automatically to 8888
    if (colorType == KARGB_4444_SKColorType) {
        colorType = kN32_SKColorType;
    }
    ...
}

使用 RGB_565 相比使用 ARGB_8888 在内存占用上减少了一半。

所以可以 根据应用场景选择不同的像素点数据格式,比如在对颜色要求不高的场景使用 RGB_565 替换默认的 ARGB_8888 就可以有效的降低 Bitmap 占用的内存。

inBitmap 复用图片内存

inBitmap 能做到重复利用图片内存减少内存分配,但在不同的系统版本有不同的条件:

在 Android 4.4 以前如果要复用 Bitmap 只能做到同等复用,即一张 1920x1080 ARGB_8888 的图片要复用只能是相同的分辨率和像素点数据格式才可以

在 4.4 及以后没有这个限制,但有一个前提条件,就是被复用的 Bitmap 内存区域要比即将分配的 Bitmap 的内存区域大。即大图片的内存可以被小图片复用,小图片的内存肯定不能被大内存的复用

private BitmapPool bitmapPool;

public Bitmap decodeBitmap(Context context, File file, int reqWidth, int reqHeight) {
    bitmapPool = GlideApp.get(context).getBitmapPool();

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(file.getAbsolutePath(), options);
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    options.inMutable = true; // 复用 inBitmap 需要将 inMutable 设置为 true
    options.inBitmap = bitmapPool.getDirty(options.outWidth, options.outHeight, options.inPreferredConfig);
    options.inJustDecodeBounds = false;
    Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
    // 处理 BitmapFactory.decodeXxx() 复用失败时,按普通方式加载处理
    if (bitmap == null && options.inBitmap != null) {
        bitmapPool.put(options.inBitmap);
        options.inBitmap = null;
        bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
    }
    if (bitmap != null && options.inBitmap != null) {
        bitmapPool.put(options.inBitmap);
    }
    return bitmap;
}

inBitmap 的原理也通过 native 源码了解:

frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

jfieldID gOptions_bitmapFieldID;

int register_android_graphics_BitmapFactory(JNIEnv* env) {
    ...
    gOptions_bitmapFieldID = GetFieldIDOrDie(env, options_class, "inBitmap",
            "Landroid/graphics/Bitmap;");
    ...
}

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    ...
    jobject javaBitmap = NULL;
    ...
    if (options != NULL) {
        ...
        // 获取到 inBitmap 设置的 Bitmap
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
        ...
    }   
    ...
    android::Bitmap* reuseBitmap = nullptr;
    unsigned int existingBufferSize = 0;
    if (javaBitmap != NULL) {
        // 调用 Bitmap.cpp 的 toBitmap() 函数
        // 拿到指向 inBitmap 的 Bitmap 的 native 指针方便操作这块内存区域
        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);
        }
    }
    ...
    // 直接返回的设置的 inBitmap
    if (javaBitmap != nullptr) {
        // 调用 Java 层的 Bitmap 的 reinit()
        bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
        outputBitmap.notifyPixelsChanged();
        // If a java bitmap was passed in for reuse, pass it back
        return javaBitmap;
    }
    ...
}

上面是当我们调用 BitmapFactory.decodeXxx() 时最终 native 的处理,可以发现当我们的 inBitmap 有值时,当然其中还会判断要复用的 Bitmap 的内存是否足够内存复用,校验通过后会将 inBitmap 设置的 Bitmap 返回,直接使用这块内存,减少了内存分配。

BitmapRegionDecoder 区域加载(大图加载方案)

在日常开发中还需要考虑网络下载的图片或从相册读取的图片是大图的情况,例如长截屏图片、全景图片这类会超过一个屏幕或超过一个控件的区域展示,如果我们还仍旧常规的使用 BitmapFactory(包括使用 Glide、Picasso) 将整个图片一次加载出来,很容易导致内存过大触发多次 GC 引发卡顿,甚至在内存不足时加载大图无法分配更多内存直接导致 OOM。

因为图片过大超过一个控件显示区域,最好的处理方式是图片只加载足够显示的区域,再结合触摸滑动的方式按区域将图片加载出来,还需要结合上面提到的 inBitmap 复用内存,这样就可以有效的降低内存占用。

图片区域加载显示 Android 已经为我们提供了 BitmapRegionDecoder:

BitmapRegionDecoder can be used to decode a rectangle region from an image. BitmapRegionDecoder is particularly useful when an original image is large and you only need parts of the image

BitmapRegionDecoder 可以将一张大图指定区域加载显示,通过 BitmapRegionDecoder.newInstance() 创建。

为了更好的理解,我们写一个简单的 demo 实现支持手势缩放拖动的大图区域加载控件:

public class ImageBigView extends AppCompatImageView
        implements View.OnTouchListener,
        GestureDetector.OnGestureListener,
        GestureDetector.OnDoubleTapListener {
    // 控件宽高
    private int mViewWidth;
    private int mViewHeight;
    // 原始图片宽高
    private int mImageWidth;
    private int mImageHeight;

    // 原始缩放比例
    private float mOriginScale;
    // 当前缩放比例
    private float mScale;

    private Bitmap mBitmap;
    private BitmapRegionDecoder mBitmapRegionDecoder; // 区域解码器
    private final BitmapFactory.Options mOptions = new BitmapFactory.Options();
    private final Rect mRect = new Rect();
    private final Matrix mMatrix = new Matrix();

    private final GestureDetector mGestureDetector;
    private final ScaleGestureDetector mScaleGestureDetector;
    private final OverScroller mScroller;

    public ImageBigView(Context context) {
        this(context, null);
    }

    public ImageBigView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ImageBigView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOnTouchListener(this);
        mGestureDetector = new GestureDetector(context, this);
        mScaleGestureDetector = new ScaleGestureDetector(context, new OnScaleListener());
        mScroller = new OverScroller(context);
    }

    public void decode(InputStream is) {
        mOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is, null, mOptions);
        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;
        mOptions.inMutable = true; // inBitmap 需要将它设置为 true
        mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        mOptions.inJustDecodeBounds = false;
        try {
            mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(is, false);
        } catch (IOException e) {
            e.printStackTrace();
        }

        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();
        mRect.left = 0;
        mRect.top = 0;
        // 因为是大图会超过控件区域,所以初始显示区域就是控件宽高
        mRect.right = Math.min(mImageWidth, mViewWidth);
        mRect.bottom = Math.min(mImageHeight, mViewHeight);
        mOriginScale = mViewWidth / (float) mImageWidth;
        mScale = mOriginScale;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mBitmapRegionDecoder != null) {
            mOptions.inBitmap = mBitmap; // 复用 Bitmap
            mMatrix.setScale(mScale, mScale);
            mBitmap = mBitmapRegionDecoder.decodeRegion(mRect, mOptions);
            canvas.drawBitmap(mBitmap, mMatrix, null);
        }
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // 将手势和缩放接管
        mGestureDetector.onTouchEvent(event);
        mScaleGestureDetector.onTouchEvent(event);
        return true; // 需要返回 true 拦截触摸事件
    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        mRect.offset((int) distanceX, (int) distanceY);
        // 显示区域 bottom 大于原始图片底部
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = mImageHeight;
            mRect.top = mImageHeight - (int) (mViewHeight / mScale);
        }
        // 显示区域 top 超过原始图片顶部
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = (int) (mViewHeight / mScale);
        }
        // 显示区域 left 超过原始图片左边
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = (int) (mViewWidth / mScale);
        }
        // 显示区域 right 超过原始图片右边
        if (mRect.right > mImageWidth) {
            mRect.right = mImageWidth;
            mRect.left = mImageWidth - (int) (mViewWidth / mScale);
        }
        invalidate();
        return true;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        mScroller.fling(
                mRect.left,
                mRect.top,
                (int) velocityX,
                -(int) velocityY,
                0,
                mImageWidth - (int) (mViewWidth / mScale), 
                0,
                mImageHeight - (int) (mViewHeight / mScale));
        return false;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.isFinished()) return;

        if (mScroller.computeScrollOffset()) {
            mRect.top = mScroller.getCurrY();
            mRect.bottom = mRect.top + (int) (mViewHeight / mScale);
            invalidate();
        }
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }

    private class OnScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scale = mScale;
            scale += detector.getScaleFactor() - 1;
            if (scale <= mOriginScale) {
                scale = mOriginScale;
            } else if (scale > mOriginScale * 2) { // 设置最大缩放比例为原始缩放比例的2倍
                scale = mOriginScale * 2;
            }
            mRect.right = mRect.left + (int) (mViewWidth / scale);
            mRect.bottom = mRect.top + (int) (mViewHeight / scale);
            mScale = scale;
            invalidate();
            return super.onScale(detector);
        }
    }
}


ImageBigView imageView = findViewById(R.id.image);
try (InputStream is = getAssets().open("test.png")) {
    imageView.decode(is);
} catch (IOException e) {
    e.printStackTrace();
}
image.png

当然 demo 上面的还有很多值得优化的地方,例如图片解析没有放在子线程、没有拖动流畅的动画效果等等,但是用于了解大图加载的核心原理是足够的。

大图加载也有对应的开源库 subsampling-scale-image-view,实现原理也是一样的并且完善了刚才说的一些问题,在实际项目中可以使用它做大图加载。

inScaled、inDensity、inTargetDensity 图片存放在合适的 drawable 目录

项目开发中会遇到各种屏幕适配的问题,根据不同的手机分辨率要将图片存放在对应的 drawable 文件夹目录下。将分辨率为 1080x2160 的图片,分别存放在 drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxhdpi ,手机分辨率也是 1080x2160,所占用的内存也有所不同:

Bitmap ldpiBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.background_ldpi);
if (ldpiBitmap!= null) {
    Log.i(TAG, "width: "  + ldpiBitmap.getWidth() + ", height: " + ldpiBitmap.getHeight() + ", ldpi bitmap byte: " + formatSize(ldpiBitmap.getAllocationByteCount()));
}

Bitmap mdpiBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.background_mdpi);
if (mdpiBitmap != null) {
    Log.i(TAG, "width: "  + mdpiBitmap.getWidth() + ", height: " + mdpiBitmap.getHeight() + ", mdpi bitmap byte: " + formatSize(mdpiBitmap.getAllocationByteCount()));
}

Bitmap hdpiBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.background_hdpi);
if (hdpiBitmap != null) {
    Log.i(TAG, "width: "  + hdpiBitmap.getWidth() + ", height: " + hdpiBitmap.getHeight() + ", hdpi bitmap byte: " + formatSize(hdpiBitmap.getAllocationByteCount()));
}

Bitmap xhdpiBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.background_xhdpi);
if (xhdpiBitmap != null) {
    Log.i(TAG, "width: "  + xhdpiBitmap.getWidth() + ", height: " + xhdpiBitmap.getHeight() + ", xhdpi bitmap byte: " + formatSize(xhdpiBitmap.getAllocationByteCount()));
}

Bitmap xxhdpiBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.background_xxhdpi);
if (xxhdpiBitmap != null) {
    Log.i(TAG, "width: "  + xxhdpiBitmap.getWidth() + ", height: " + xxhdpiBitmap.getHeight() + ", xxhdpi bitmap byte: " + formatSize(xxhdpiBitmap.getAllocationByteCount()));
}

Bitmap xxxhdpiBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.background_xxxhdpi);
if (xxxhdpiBitmap != null) {
    Log.i(TAG, "width: "  + xxxhdpiBitmap.getWidth() + ", height: " + xxxhdpiBitmap.getHeight() + ", xxxhdpi bitmap byte: " + formatSize(xxxhdpiBitmap.getAllocationByteCount()));
}

private String formatSize(long byteSize) {
    return Formatter.formatFileSize(this, byteSize);
}

width: 4320, height: 8640, ldpi bitmap byte: 149MB
width: 3240, height: 6480, mdpi bitmap byte: 83.98MB
width: 2160, height: 4320, hdpi bitmap byte: 37.32MB
width: 1620, height: 3240, xhdpi bitmap byte: 21.00MB
width: 1080, height: 2160, xxhdpi bitmap byte: 9.33MB
width: 810, height: 1620, xxxhdpi bitmap byte: 5.25MB

你会发现图片放在不同 drawable 目录,Bitmap 占用的内存大小都有所不同。所以在实际项目中,最好是根据不同的分辨率存储一份本地图片;如果为了减少包体积考虑只存储一份本地图片,也要做好能适配到市面上大部分机型的分辨率目录,减少加载图片的内存占用。

我们需要分析 BitmapFactory.decodeResource() 的源码:

BitmapFactory.java

public static Bitmap decodeResource(Resources res, int id, Options opts) {
    validate(opts);
    Bitmap bm = null;
    InputStream is = null;
        
    try {
        final TypeValue value = new TypeValue();
        is = res.openRawResource(id, value);
    
        bm = decodeResourceStream(res, value, is, null, opts);  
    } catch (Exception e) {
        // do nothing
    } finally {
        try {
            if (is != null) is.close();
        } catch (IOException e) {
            // Ignore
        }
    }
        
    if (bm == null && opts != null && opts.inBitmap != null) {
        throw new IllegalArgumentException("Problem decoding into existing bitmap");
    }
    
    return bm;
}

@Nullable
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();
    }

    // 设置 inDensity、inTargetDensity 的值
    // DisplayMetrics.DENSITY_DEFAULT=160
    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);
}
        
@Nullable
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
        @Nullable 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;
}

private static Bitmap decodeStreamInternal(@NonNull InputStream is,
        @Nullable Rect outPadding, @Nullable Options opts) {
    ...
    return nativeDecodeStream(is, tempStorage, outPadding, opts);
}


private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
        Rect padding, Options opts);

frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

jfieldID gOptions_scaledFieldID;
jfieldID gOptions_densityFieldID;
jfieldID gOptions_screenDensityFieldID;
jfieldID gOptions_targetDensityFieldID;

int register_android_graphics_BitmapFactory(JNIEnv* env) {
    ...
    gOptions_scaledFieldID = GetFieldIDOrDie(env, options_class, "inScaled", "Z");
    gOptions_densityFieldID = GetFieldIDOrDie(env, options_class, "inDensity", "I");    
    gOptions_screenDensityFieldID = GetFieldIDOrDie(env, options_class, "inScreenDensity", "I");
    gOptions_targetDensityFieldID = GetFieldIDOrDie(env, options_class, "inTargetDensity", "I");    
    ... 
}

static const JNINativeMethod gMethods[] = {
    {   "nativeDecodeStream",
        "(Ljava/io/InputStream;[BLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
        (void*)nativeDecodeStream
    }
    ...
}

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {

    ...
    if (stream.get()) {
        ...
        bitmap = doDecode(env, bufferedStream.release(), padding, options);
    }
    return bitmap;
}

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    ...
    float scale = 1.0f;
    ...
    if (options != NULL) {
        // 如果 BitmapFactory.Options 设置 inScaled=true
        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) {
                scale = (float) targetDensity / density; // 计算缩放比例
            }
        }       
    }
    ...
    // Determine the output size.   
    SkISize size = codec->getSampledDimensions(sampleSize);
    
    int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false; 

    // Apply a fine scaling step if necessary.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
        willScale = true;
        scaledWidth = codec->getInfo().width() / sampleSize;
        scaledHeight = codec->getInfo().height() / sampleSize;
    }
    ...

    // Scale is necessary due to density differences.
    if (scale != 1.0f) {
        willScale = true;
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }

    ...
    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());
        ...
        
        SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
        canvas.scale(sx, sy);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } 
    ... 
}

在使用 Canvas 绘制 Bitmap 前会根据 density 和 targetDensity 计算缩放比例,两个参数都是从 BitmapFactory.Options 获取:
1.density 对应的 inDensity:Bitmap 的自身密度、分辨率
2.targetDensity 对应的 inTargetDensity:Bitmap 最终绘制的目标位置的分辨率

其中 inDensity 和图片存放的目录有关,同一张图片放置在不同的 drawable 目录会有不同的值:

dpiFolder ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi
density 0.75 1 1.5 2 3 4
densityDpi 120 160 240 320 480 640

结合代码和图表,图片转换规则如下:

新图的宽度 = 原图宽度 * (设备dpi / 目录对应dpi)
新图的高度 = 原图高度 * (设备dpi / 目录对应dpi)

不同手机设备也有对应的dpi表格:

密度(densityFolder) ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi
倍数关系(density) 0.75 1 1.5 2 3 4
密度数(densityDpi) 120 160 240 320 480 640
分辨率 240x320 320x480 480x800 720x1280 1080x1920 3840x2160

Android 以 160dpi 作为基准(drawable 没有带后缀的目录也是 160dpi),根据上表显示,在一张 1080x2160、像素点数据格式 ARGB_8888 的图片放在不同的 drawable 目录,手机分辨率为 1080x2160 即设备为 480dpi,粗略计算结果:

// mdpi
理论计算结果:
width = 1080 * (480 / 160) = 3240
height = 2160 * (480 / 160) = 6480
memory = 3240 * 6480 * 4byte / 1024 / 1024 ≈ 80.09MB 
程序计算结果:
width: 3240
height: 6480
mdpi bitmap byte: 83.98MB

// hdpi
理论计算结果:
width = 1080 * (480 / 240) = 2160
height = 2160 * (480 / 240) = 4320
memory = 2160 * 4320 * 4byte / 1024 / 1024 ≈ 35.59MB
程序计算结果:
width: 2160
height: 4320
hdpi bitmap byte: 37.32MB

// xdpi
理论计算结果:
width = 1080 * (480 / 320) = 1620
height = 2160 * (480 / 320) = 3240
memory = 1620 * 3240 * 4byte / 1024 / 1024 ≈ 20.02MB
程序计算结果:
width: 1620
height: 3240
xhdpi bitmap byte: 21.00MB
    
// xxdpi
理论计算结果:
width = 1080 * (480 / 480) = 1080
height = 2160 * (480 / 480) = 2160
memory = 1080 * 2160 * 4byte / 1024 / 1024 ≈ 8.89MB
程序计算结果:
width: 1080
height: 2160
xxhdpi bitmap byte: 9.33MB

// xxxdpi
理论计算结果:
width = 1080 * (480 / 810) = 810
height = 2160 * (480 / 640) = 1620
memory = 810 * 1620 * 4byte / 1024 / 1024 ≈ 5MB
程序计算结果:
width: 810
height: 1620
xxxhdpi bitmap byte: 5.25MB

如果我们需要自己计算缩放比例,首先要将 inScaled 设置为 true,此时就会根据提供的 inDensity、inScreenDensity 和 inTargetDensity 计算缩放比例。

CompressFormat 选择合适的压缩方式

通过 Bitmap.compress(CompressFormat format, int quality, OutputStream stream) 设置压缩图片。其中的参数 quality 为图片的品质,取值为 0-100,值越低图片失真越严重,100 代表最高品质不压缩。另外,类似 PNG 这种无损格式会忽略 quality 的设置。stream 为图片被压缩后保存的输出流。

其中,在图片压缩方式上,Android为我们提供了三种方式:

Bitmap.CompressFormat.JPEG:表示以 JPEG 压缩算法进行图像压缩,压缩后的格式可以是 jpg 或 jpeg,是一种有损压缩

Bitmap.CompressFormat.PNG:表示以 PNG 压缩算法进行图像压缩,压缩后的格式可以是 png,是一种无损压缩

Bitmap.CompressFormat.WEBP:表示以 WebP 压缩算法进行图像压缩,压缩后的格式可以是 webp,是一种有损压缩。质量相同情况下,WebP 格式图像的体积要比 JPEG 格式图像小 40%,但 WebP 格式图像的编码时间比 JPEG 格式图像长 8 倍

下面分别对三种压缩方式举例,图片分辨率为 1080x2160,大小为 1.08MB 的 jpg 图片放在外部存储:

Bitmap jpegCompressBitmap = compress(file, Bitmap.CompressFormat.JPEG);
if (jpegCompressBitmap != null) {
    File jpegFile = saveBitmap(jpegCompressBitmap, Bitmap.CompressFormat.JPEG, new File(Environment.getExternalStorageDirectory() + "/Android/data/" + getPackageName() + "/image_jpeg.jpeg"));
    Log.i(TAG, "jpeg file size = " + formatSize(jpegFile.length()) + ", bitmap byte: " + formatSize(jpegCompressBitmap.getAllocationByteCount()));
}

Bitmap pngCompressBitmap =  compress(file, Bitmap.CompressFormat.PNG);
if (pngCompressBitmap != null) {
    File pngFile = saveBitmap(pngCompressBitmap, Bitmap.CompressFormat.PNG, new File(Environment.getExternalStorageDirectory() + "/Android/data/" + getPackageName() + "image_png.png"));
    Log.i(TAG, "png file size = " + formatSize(pngFile.length()) + ", bitmap byte: " + formatSize(pngCompressBitmap.getAllocationByteCount()));
}

Bitmap webpCompressBitmap = compress(file, Bitmap.CompressFormat.WEBP);
if (webpCompressBitmap != null) {
    File webpFile = saveBitmap(webpCompressBitmap, Bitmap.CompressFormat.WEBP, new File(Environment.getExternalStorageDirectory() +"/Android/data/" + getPackageName() + "image_webp.webp"));
    Log.i(TAG, "webp file size = " + formatSize(webpFile.length()) + ", bitmap byte: " + formatSize(webpCompressBitmap.getAllocationByteCount()));
}
        
private File saveBitmap(Bitmap bitmap, Bitmap.CompressFormat format, File target) {
    if (target.exists()) {
        target.delete();
    }

    try (FileOutputStream fos = new FileOutputStream(target)) {
        bitmap.compress(format, 50, fos);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return target;
}

private Bitmap compress(File file, Bitmap.CompressFormat format) {
    Bitmap sourceBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    sourceBitmap.compress(format, 50, baos);
    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    return BitmapFactory.decodeStream(bais);
}

private String formatSize(long byteSize) {
    return Formatter.formatFileSize(this, byteSize);
}

运行结果:
jpeg file size = 94.62kB, bitmap byte: 9.33MB
png file size = 2.03MB, bitmap byte: 9.33MB
webp file size = 45.89kB, bitmap byte: 9.33MB

Bitmap.compress() 可以压缩图片,但压缩的是存储大小即图片存放在磁盘的大小。当图片从外部和 app 内被加载到内存中转换为 Bitmap 时,只和图片分辨率和像素点数据格式有关,通过上面的 demo 也可以看到最终的 Bitmap 三种存储格式的 Bitmap 内存占用是相同的。
————————————————
版权声明:本文为CSDN博主「VincentWei95」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_31339141/article/details/104736704

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容