Android图片加载内存占用分析

作为一名Android开发人员,你见得最多的大概就是res/drawable-[density]/ 文件夹了,现在又大概多了 res/mipmap-[density]/ 文件夹,这些文件夹通常用来存放图片资源文件,大家可能再熟悉不过了,现在我问你,一张大小为376.16K的480x800且位数为8的图片放在res/drawable-xxhdpi/ 文件夹下,在分辨率为1920*1080的手机上这张图片占用的内存是多少?

1 概念厘清

如果对此比较有了解的小傻逼们,后面其实不需要看了,纯粹来扫一下盲,在正式分析之前,先来厘清一下相关的概念。

  • 1.1屏幕尺寸:按屏幕对角测量的实际物理尺寸,例如5.5英寸。Android 将所有实际屏幕尺寸分组为四种通用尺寸:小、 正常、大和超大;
  • 1.2分辨率:屏幕上物理像素的总数,添加对多种屏幕的支持时, 应用不会直接使用分辨率,而只应关注通用尺寸和密度组指定的屏幕尺寸及密度
  • 1.3屏幕密度:屏幕物理区域中的像素量,通常称为 dpi(每英寸点数)。屏幕密度越低在给定物理区域的像素就会较少。Android 将所有屏幕密度分为六组通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高);
  • 1.4密度无关像素 (dp):在定义 UI 布局时应使用的虚拟像素单位。密度无关像素等于 160 dpi 屏幕上的一个物理像素,这是系统为mdpi(中)密度屏幕假设的基线密度。在运行时,系统根据使用中屏幕的实际密度按需要以透明方式处理dp单位的任何缩放 。dp单位转换为屏幕像素很简单: px = dp * (dpi / 160)。 例如,在 240 dpi屏幕上,1 dp等于1.5 物理像素。

对于我们的分析比较重要的就是屏幕密度。

2 屏幕密度(dpi)对应关系

通用密度 ldpi mdpi(基线密度) hdpi xhdpi xxhdpi xxxhdpi
描述 超高 超超高 超超超高
大小(单位dpi) 120 160 240 320 480 640
缩放系数 0.75 1 1.5 2 3 4

六种通用密度之间遵循 3:4:6:8:12:16 的缩放比率,要注意的一点是xxxhdpi仅限启动器图标

3 具体分析实现代码

代码很简单,就是用一个ImageView包含一张背景图片,然后通过转换为Bitmap查看占用内存大小。
布局文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.xishuang.imagesizetest.MainActivity">

    <ImageView
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/bg2" />

</FrameLayout>

布局文件,就是一个ImageView控件,包含一张背景图。

MainAcivity.java

private void printBitmapSize(ImageView imageView) {
        Drawable drawable = imageView.getDrawable();
        if (drawable != null) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            Bitmap bitmap = bitmapDrawable.getBitmap();
            //API 19
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
                size = bitmap.getAllocationByteCount();
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1){
                //API 12
                size = bitmap.getByteCount();
            } else {
                //earlier version
                size = bitmap.getRowBytes() * bitmap.getHeight();
            }
            Log.d(TAG, " size = " + size);
        } else {
            Log.d(TAG, "Drawable is null !");
        }
    }

getAllocationByteCount()方法可以获取图片的实际占用内存大小,在此之前得介绍一种特殊的res/drawable-[density]/文件夹,就是res/drawable-nodpi/,不管当前屏幕的密度如何,系统都不会缩放以此限定符标记的资源。意思就是在这个文件夹中的图片按原样进行展示,不会像其它的res/drawable-[density]/那样改变文件的大小,我们就以此为基准进行分析。

4 图片的实际内存占用

以实际例子作为分析,把一张大小为376.16K的480x800且位数为8的图片为例
图片的像素总数 480x800 = 384000
先使用压缩前的图片为例,分析图片占用的内存大小并打印出来


压缩前磁盘占用大小

压缩前内存大小

对图片进行压缩后进行同样得操作

压缩后磁盘占用大小

压缩后内存占用大小

很明显,压缩前后内存的占用大小同样为1536000(Byte),说明图片的磁盘占用大小与图片的内存或显存占用没有必然关系。从而说明压缩图片可以减少我们得apk大小,但是内存的占用是不会变小的,那么图片的内存占用与什么有关系呢?继续。。。

图片的内存占用大小为1536000(Byte),而图片的原始图片像素总数为384000,一眼看过去好像没啥关系,但是真相是384000 * 4 = 1536000(Byte),原始图片尺寸大小与最终的内存占用大小呈倍数的关系,所以在这里与内存占用大小有直接关系的就是原始图片尺寸大小(例如:480x800),道理我都懂,但是倍数关系是从哪里来的呢,这就要谈论到Bitmap的像素格式了。

Android系统支持4种格式的像素格式,源码在Bitmap.Config中

/**
     * 可用的bitmap配置, 一个bitmap配置描述的是每个像素的存储格式,这将会影响到图片的质量 (颜色深
     * 度) 以及显示透明/半透明颜色的能力
     */
    public enum Config {
        // 这些枚举中的值必须要与Skia图像引擎的SkBitmap.h中对应值一一对应

        /**
         * 只有一个alpha通道 
         * 每个像素占1个字节
         */
        ALPHA_8     (1),

        /**
         *每个像素占用2个字节,只有RGB 3个通道,没有alpha 通道
         * 红色的精度是5 bits, 绿色精度是6 bits,蓝色精度是5
         */
        RGB_565     (3),

        /**
         * 每个像素占用2个字节. 
         * (虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃)
         */
        @Deprecated
        ARGB_4444   (4),

        /**
         * 每个像素占用4个字节. 每个通道 (RGB的3个通道和alpha
         * 的1个透明度通道) 的进度是8bit (256个可能值)
         * 这种配置是最灵活的, 质量最好,尽量使用这种格式.
         */
        ARGB_8888   (5);
    }

由于官方默认使用ARGB_8888格式,导致图片的每个像素会占用4个Byte大小,所以最终的图片占用内存大小就是像素总数*像素格式,放到例子里头就是384000 * 4 = 1536000(Byte),成功接上去了,哈哈哈。。。

小结论:图片的直接内存占用和图片的像素总数和系统的像素格式相关,与磁盘存储的图片大小无关,其实与磁盘存储的图片位数也无关。

5 Android对在res/drawable-[density]/ 文件夹中图片进行的骚操作

前面提到的图片实际占用内存大小,是很合理的,但是图片是放置在


nodpi.png

前面也已经提到过res/drawable-nodpi/文件夹,在这个文件夹中的图片按原样进行展示,不会像其它的res/drawable-[density]/那样改变文件的大小,类似于从SD卡或者网络直接加载一张图片。
但是如果把图片放在其它的res/drawable-[density]/ 文件夹中的话,事情就会变得有些不一样了,系统会根据手机的屏幕密度来缩放对应文件夹中的图片。

下面就是测试结果,测试手机为360 vizza,手机分辨率为1920*1080,屏幕密度为480dpi,测试图片为480x800的图片。
先把图片放置drawable-ldpi中看占用内存大小,然后依次类比,得出最终的对比数据。

文件夹 文件夹dpi size(Byte)
drawable-ldpi 120 24576000
drawable-mdpi 160 13824000
drawable-hdpi 240 6144000
drawable-xhdpi 320 3456000
drawable-xxhdpi 480 1536000
drawable-xxxhdpi 640 864000

看到这个结果先不要慌,稳住,我们能赢...

经过前面的分析,我们知道在res/drawable-nodpi/下图片的占用内存为1536000(Byte),发现没有,我加粗的那一行数据中,也就在当图片放置在res/drawable-xxhdpi/文件夹下面时,图片所占用的内存也是1536000(Byte),而我们得测试机的屏幕密度就是480dpi,说明在对应屏幕密度的文件下获取图片时内存占用不会有变化。
而在把图片放置其他对应dpi文件夹下时,会出现图片内存占用出现不同程度的缩放,我们称与手机屏幕密度一致的文件夹称之为目标文件夹,当图片放置的文件夹对应密度比目标文件夹越小时,图片占用内存越大,当图片放置的文件夹对应密度比目标文件夹越大时,图片占用内存越小。

还记得这个表吗

通用密度 ldpi mdpi(基线密度) hdpi xhdpi xxhdpi xxxhdpi
描述 超高 超超高 超超超高
大小(单位dpi) 120 160 240 320 480 640
缩放系数 0.75 1 1.5 2 3 4

六种通用密度之间遵循 3:4:6:8:12:16 的缩放比率,内存占用缩放的秘密其实就是在这个缩放比率当中,最终的图片占用内存大小为:
图片最终内存=图片原始内存 * (手机屏幕密度/资源图片文件密度) ^ 2
其实就是图片宽和高都按缩放比率进行对应的缩放。

举个栗子:
当图片放置在res/drawable-ldpi/文件夹下时,图片内存为1536000(480/120)^2=153600016=24576000(Byte);
当图片放置在res/drawable-xxhdpi/文件夹下时,图片内存为1536000(480/480)^2=15360001=1536000(Byte);
当图片放置在res/drawable-xxxhdpi/文件夹下时,图片内存为1536000(480/640)^2=15360000.5625=864000(Byte);
注:res/drawable-xxxhdpi/文件夹官方建议只能放启动图标,这里只是为了测试才放置测试图片。
对比一下上表对比数据,都一一对应,说明是ok的。

然后最终结论就是
1、图片的直接内存占用和图片的像素总数和系统的像素格式相关,与磁盘存储的图片大小无关,其实与磁盘存储的图片位数也无关,图片的直接内存占用大小为:像素总数 * 像素的格式(像素的格式其实就是确定了每个像素占用的字节数)
2、图片放置在res/drawable-[density]/ 文件夹中时,图片占用内存大小为:图片最终内存 = 图片原始内存 * (手机屏幕密度/资源图片文件密度) ^ 2

推荐阅读更多精彩内容