Android图片占用内存的计算

首先明确两个问题:

  1. 图片大小和占用内存大小没有关系,图片大小之关系到apk的大小
  2. webp虽然图片小,占用内存方面和其他图片没有性能上的优势

几个基本概念

  • px:像素(pixel),指的是屏幕上的物理点,最小的独立显示单位
  • ppi:每英寸像素点(Pixels Per Inch)
  • dpi:每英寸点(Dots Per Inch)
  • dp:像素无关点(Density-Independent pixel),这个Android定义的虚拟值,和px关系式是px = dp * (dpi / 160)

        ppidpi经常都会出现混用现象。它们是用来描述屏幕的属性,或者说是性能。从技术角度说,“像素”(P)只存在于计算机显示领域,而“点”(d)只出现于打印或印刷领域。

为什么规定160dpi规格的屏幕上,1dp = 1px?
这个在Google的官方文档中有给出了解释,因为第一款Android设备(HTC的T-Mobile G1)是属于160dpi的。

聊一个问题

设备一:Sony Z2 屏幕尺寸:5.2in 屏幕分辨率:1080*1920 DPI:424 
设备二:华为 Mate 7 屏幕尺寸:6.0in 屏幕分辨率:1080*1920 DPI:367
比如一个要32dp的高度的控件,按照公式
Z2 32dp = 32 * (424/160)= 84.8px
Mate 7 32dp = 32*(367/160) = 73.4px

明显大小不一样啊,why?
        实际上们计算计算的dpi并不是公式里面的dpi,dpi只有120,160,240,320,480,640几种,可以通过系统api获取getResources().getDisplayMetrics().densityDpi,我们获取的dpi实际上只要处于任一个系统dpi范围内即可,比如计算得到的367和424都属于320~480dpi范围内,属于scale = 3x,所以计算公式实际是32 *(480/160) = 32*3 = 96px。两个手机的大小实际上是一致的。

内存的计算

理论上的内存大小:
图片占用内存 = 宽度像素 * 高度像素 * 单个像素占的字节数

单个像素占的字节数是和安卓色彩模式有关系的,如下:
Android中的四种色彩模式:

ALPHA_8:  每个像素占用1byte内存
ARGB_4444:每个像素占用2byte内存
ARGB_8888:每个像素占用4byte内存
RGB_565:     每个像素占用2byte内存
        注:ARGB指的是一种色彩模式,里面A代表Alpha,R表示red,G表示green,B表示blue,其实所有的可见色都是红绿蓝组成的,所以红绿蓝又称为三原色。Android默认的bitmap色彩模式是ARGB_8888。通过bitmap源码可以追踪看到单个像素占的字节数和色彩模式是有关系的,通过Bitmap源码查看getRowBytes方法,这里源码暂时不放了,意义不大,知道就好。
        实际上加载图片时,是通过BitmapFactory的decodeResource方法
 /**
     * @param res   包含图片资源的Resources对象,一般通过getResources()即可获取
     * @param id    资源文件id, 如R.mipmap.ic_laucher
     * @param opts  可为空,控制采样或图片是否需要完全解码还是只需要获取图片大小
     * @return      解码的bitmap
     */
    public static Bitmap decodeResource(Resources res, int id, Options opts) {
        Bitmap bm = null;
        InputStream is = null; 

        try {
            final TypedValue value = new TypedValue();
            //1.读取资源id,返回流格式
            is = res.openRawResource(id, value);
            //2. 直接加载数据流格式进行解码,一般opts为空
            bm = decodeResourceStream(res, value, is, null, opts);
        } catch (Exception e) {

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

    可以看到在拿到资源图片后,转换成Inputstream,交由decodeResourceStream处理,见下面:

    /**
     * 根据输入的数据流确码成一个新的bitmap, 数据流是从资源处获取,在这里可以根据规则对图片进行一些缩放操作
     */
    public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {
        if (opts == null) {//如果没有设置Options,系统会新创建一个Options对象
            opts = new Options();
        }
        //若没有设置opts,inDensity就是初始值0,它代表图片资源密度
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) { 
               //如果density等于0,则采用默认值160
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                //如果没有设置资源密度,则图片不会被缩放
                //这里density的值对应的就是资源密度值,即图片文件夹所代表的的密度
                opts.inDensity = density;
            }
        }
        //此时inTargetDensity默认也为0
        if (opts.inTargetDensity == 0 && res != null) {
            //将手机的屏幕密度值赋值给最终图片显示的密度
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }

        return decodeStream(is, pad, opts);
    }
这里调用了native层的decodeStream方法,下面是该方法源码;

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);
        //最终加载的图片的密度
        const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
        //手机的屏幕密度
        const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
        //如果资源密度不为0,手机屏幕密度也不为0, 资源的密度与屏幕密度不相等时,图片缩放比例=屏幕密度/资源密度
        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");
}
//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);//这里+0.5是保证在图片缩小时,可能会出小数,这里加0.5是为了让除后的数向上取整
    scaledHeight = int(scaledHeight * scale + 0.5f);
}
if (willScale) {
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());

    // 设置解码图片的colorType
    SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
     //设置图片的宽高
    outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
            colorType, decodingBitmap.alphaType()));
    if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
        return nullObjectReturn("allocation failed for scaled bitmap");
    }

    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);//将图片画到画布上
}

主要注意两个地方:
这是获取缩放比例

 scale = (float) targetDensity / density;

这里明确的看到放大系数的算法是 屏幕密度 / 文件夹密度
targetDensity 没有赋值的话,就是屏幕密度,
density 资源文件夹代表的密度

另:如果想更改调节缩放比例,这两个参数必须同时设置才有效

计算缩放后的图片大小
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
}

        到这里其实我们看到,图片占用内存不仅和图片本身大小有关系,还和屏幕密度和图片所在的资源文件夹都有关系
公式实际是:
图片占用内存 = 宽度 * 高度 * (屏幕密度 / 资源文件夹密度)^2 * 单个像素占的字节数

MemorySize ≈ (width * scale) * (height * scale) * 每个像素需要的字节数 ≈ width * height * scale ^ 2 * 每个像素需要的字节数

总结:

  1. 开发过程中只切一套大图放在高分辨率文件夹下(3x或者4x),是可行的,从公式可看出,使用同一个设备时,drawable表示的分辨率越高,则图片占用的内存越小,反之越大。所以,在做图片的兼容性时,如果只想使用一张图片,则应使用3倍甚至4倍的图片(3倍是主流机型,但在4倍手机上会被放大,图片可能失真),这样在低分辨率的手机上,不仅显示清晰,而且系统会自动进行缩放,从而确保占用较小的内存。
  2. 图片占用内存和设备的分辨率和资源所在的文件夹是有关系的

Android手机屏幕标准
对应图标尺寸标准      
屏幕密度 (densityDpi)scale
xxxhdpi 3840*2160
192*192
640
4
xxhdpi 1920*1080
144*144
4803
xhdpi  1280*720  
96*96
3202
hdpi   480*800
72*72
2401.5
mdpi   480*320
48*48
1601
ldpi   320*240
36*36  
1200.75

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ezview_uniview

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值