iis服务器助手广告广告
返回顶部
首页 > 资讯 > 移动开发 >Android中一张图片占用的内存大小
  • 728
分享到

Android中一张图片占用的内存大小

图片Android 2022-06-06 13:06:49 728人浏览 独家记忆
摘要

最近面试过程中发现对Android中一些知识有些模棱两可,之前总是看别人的总结,自己没去实践过,这两天对个别问题进行专门研究 探讨:如何计算An

最近面试过程中发现对Android中一些知识有些模棱两可,之前总是看别人的总结,自己没去实践过,这两天对个别问题进行专门研究

探讨:如何计算Android中一张图片占据内存的大小

解释:此处说的占据内存,是APP加载图片用的内存,即APP运行时图片占用的内存,不考虑图片显示到界面占用的内存(即只加载图片,不显示。因为显示图片时,系统有可能根据控件的大小或其他因素对内存进行了优化,不确定因素太多),同时也不考虑使用三方库加载图片占用的内存,如glide、等三方图片库,三方库在加载图片的过程中有可能对图片进行了优化,会<=图片加载占用的内存。
将通过BitmapFactory加载的方式,获取bitmap的字节数,来计算图片占用的内存大小。建议,为了避免因系统对bitmap对象的复用(有可能)导致的bitmap字节数的偏差,每次获取对象大小前,最好杀死应用,从新打开App。
本片博客讲诉的内容,有不正确的地方,欢迎大家斧正。由于很多博客都已经讲过这个问题,这里只做基本知识梳理、补充。

图片内存大小跟占用空间大小有什么关系?

占用空间的大小不是图片占用内存的大小,占用空间是在磁盘(电脑硬盘或者手机SD卡)上占用的空间,内存大小是加载到内存中占用的内存大小。两个只是单位是一样的,但不是一个概念,不要混淆。
这里所说的图片分两类:

硬盘中的图片 res中的图片 图片占用内存 = bitmap 宽 * bitmap 高 * 每个像素占用的字节数 APP资源目录res中的图片,加载到内存时,占用的内存大小

所得结论:
公式1:

图片占用内存 = bitmap 宽 * bitmap 高 * 每个像素占用的字节数

或公式2:

图片占用的内存 = 原图宽 * (inTargetDensity / inDensity) * 原图高 * (inTargetDensity / inDensity) * 每个像素占用的字节数

即图片所有像素点占用的字节数
如果 inTargetDensity / inDensity > 1则,加载到内存 图片被放大,否则被缩小

图片占用内存 = bitmap 宽 * bitmap 高 * 每个像素占用的字节数 bitmap 宽 = 原图的宽 * (设备的 dpi / 目录对应的 dpi) bitmap 高 = 原图的高 * (设备的 dpi / 目录对应的 dpi)

注意 :如果 BitmapFactory.Options options = new BitmapFactory.Options(); 中 options.inSampleSize != 0 ,则以上公式中。默认options.inSampleSize为0,源码中关于此属性有一段注释,如果为0,则nativate层按1来取值
inSampleSize > 1时

bitmap 宽 = (原图宽 / inSampleSize) * (inTargetDensity / inDensity) bitmap 高 = (原图高 / inSampleSize) * (inTargetDensity / inDensity)

(注意inSampleSize只能是2的幂,如不是2的幂下转到最大的2的幂,而且inSampleSize>=1)
BitmapFactory.decodeResource 加载的图片可能会经过缩放
查看android 2.3的系统源码 可以发现 float scale = targetDensity / (float)density 相关的信息。android 比较高的系统(8.0 9.0),Bitmap分配内存相关的细节放到了native层,没有细看。如下是2.3系统相关的代码
https://www.androidos.net.cn/android/2.3.7_r1/xref/frameworks/base/graphics/java/android/graphics/BitmapFactory.java

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
        // we don't throw in this case, thus allowing the caller to only check
        // the cache, and not force the image to be decoded.
        if (is == null) {
            return null;
        }
        // we need mark/reset to work properly
        if (!is.markSupported()) {
            is = new BufferedInputStream(is, 16 * 1024);
        }
        // so we can call reset() if a given codec gives up after reading up to
        // this many bytes. FIXME: need to find out from the codecs what this
        // value should be.
        is.mark(1024);
        Bitmap  bm;
        if (is instanceof AssetManager.AssetInputStream) {
            bm = nativeDecodeAsset(((AssetManager.AssetInputStream) is).getAssetInt(),
                    outPadding, opts);
        } else {
            // pass some temp storage down to the native code. 1024 is made up,
            // but should be large enough to avoid too many small calls back
            // into is.read(...) This number is not related to the value passed
            // to mark(...) above.
            byte [] tempStorage = null;
            if (opts != null) tempStorage = opts.inTempStorage;
            if (tempStorage == null) tempStorage = new byte[16 * 1024];
            bm = nativeDecodeStream(is, tempStorage, outPadding, opts);
        }
        return finishDecode(bm, outPadding, opts);
    }
    private static Bitmap finishDecode(Bitmap bm, Rect outPadding, Options opts) {
        if (bm == null || opts == null) {
            return bm;
        }
        final int density = opts.inDensity;
        if (density == 0) {
            return bm;
        }
        bm.setDensity(density);
        final int targetDensity = opts.inTargetDensity;
        if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
            return bm;
        }
        byte[] np = bm.getNinePatchChunk();
        final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
        if (opts.inScaled || isNinePatch) {
            float scale = targetDensity / (float)density;
            // TODO: This is very inefficient and should be done in native by Skia
            final Bitmap oldBitmap = bm;
            bm = Bitmap.createScaledBitmap(oldBitmap, (int) (bm.getWidth() * scale + 0.5f),
                    (int) (bm.getHeight() * scale + 0.5f), true);
            oldBitmap.recycle();
            if (isNinePatch) {
                np = nativeScaleNinePatch(np, scale, outPadding);
                bm.setNinePatchChunk(np);
            }
            bm.setDensity(targetDensity);
        }
        return bm;
    }
设备dpi

即设备(这里指手机)的像素密度,代码里指 inTargetDensity,只跟设备有关
例如:买手机时候查看手机屏幕分辨率(像素) 1080 * 1920 , 5.2 英寸 (手机的对角线长度)
一英寸 = 2.54 cm
则dpi = (根号下(1080 * 1080 + 1920 * 1920))/ 5.2 = 423.6 取近似值 480

目录对应的 dpi

代码里指 inDensity
目录名称与 inDensity 的对应关系如下
drawable 没带后缀对应的inDensity为 160 dpi

ldpi ——> 120 即图片放在ldpi目录时,inDensity 为 120 mdpi ——> 160 即图片放在mdpi目录时,inDensity 为 160 hdpi ——> 240 … xhdpi ——> 320 … xxhdpi ——> 480 … xxxhdpi ——> 640 …
将一张图片放入不同的res目录中 ldpi,mdpi 等,可以通过下面方式获取相应目录的dpi
private int getTargetDensityByResource(Resources resources, int id) { 
    TypedValue value = new TypedValue();  
    resources.openRawResource(id, value);  
    Log.d("LuoYer", "value.density: " + value.density);  
    return value.density;  
} 
Android中RGB编码格式(整型编码) RGB888(int):R、G、B分量各占8位 RGB565(short):R、G、B分量分别占5、6、5位 RGB555(short):RGB分量都用5位表示(剩下的1位不用) ARGB8888(int):A、R、G、B分量各占8位 ARGB4444(short):A、R、G、B分量各占4位
android 加载bitmap时,默认编码格式 为 ARGB8888,故图片的每个像素点占 4个字节

代码:

BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.weixin, options);
DisplayMetrics metrics = new DisplayMetrics();
WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(metrics);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount()  + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
Log.i(TAG, "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
//Log.i(TAG, "imageview.width:" + imageView.getWidth() + ":::imageview.height:" +    imageView.getHeight());
Log.i(TAG, "loadResImage: options.inSampleSize:" + options.inSampleSize);
Log.i(TAG, "loadResImage: " + metrics.density);
Log.i(TAG, "loadResImage: " + metrics.heightPixels);
Log.i(TAG, "loadResImage: " + metrics.widthPixels);  
手机SD卡中的图片,加载到内存时,占用的内存大小

所得结论:

图片占用内存 = 原图宽 * 原图高 * 每个像素占用的字节数

同res中的资源 :如果 BitmapFactory.Options options = new BitmapFactory.Options(); 中 options.inSampleSize != 0 ,则以上公式中。默认options.inSampleSize为0,源码中关于此属性有一段注释,如果为0,则nativate层按1来取值
inSampleSize > 1时

bitmap 宽 = (原图宽 / inSampleSize) * 每个像素占用的字节数 bitmap 高 = (原图高 / inSampleSize) * 每个像素占用的字节数 图片占用内存 = bitmap 宽 * bitmap 高 * 每个像素占用的字节数

(注意inSampleSize只能是2的幂,如不是2的幂下转到最大的2的幂,而且inSampleSize>=1)
对于一个file或者stream那么inDensity和inTargetDensity是不考虑的!他们默认就是0,可以理解为 (inTargetDensity / inDensity) = 1。

代码:

BitmapFactory.Options options = new BitmapFactory.Options();
String str = Environment.getExternalStorageDirectory() + "/Pictures/Screenshots/Screenshot_20200221-110020.jpg";
Bitmap bitmap = BitmapFactory.decodeFile(str);
DisplayMetrics metrics = new DisplayMetrics();
WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(metrics);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount()  + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
Log.i(TAG, "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
//Log.i(TAG, "imageview.width:" + imageView.getWidth() + ":::imageview.height:" +    imageView.getHeight());
Log.i(TAG, "loadResImage: options.inSampleSize:" + options.inSampleSize);
Log.i(TAG, "loadResImage: " + metrics.density);
Log.i(TAG, "loadResImage: " + metrics.heightPixels);
Log.i(TAG, "loadResImage: " + metrics.widthPixels);   
注意:一定要动态申请SD卡读写权限,否则获取到的bitmap是null的,小编刚开始没有动态申请权限(5.0系统的手机除外),判断File 一直存在,但就是 bitmap获取为null,一会找不到问题所在,后来想到权限的问题,解决了

getByteCount() 获取的单位为 Byte(简称B),即字节,一个字节为 8bite,即8比特(简称b),
1M = 1024 KB = 1024 * 1024 B

位深与色深

我们知道了图片在内存中和在磁盘上的两种不同的表示形式:前者为Bitmap,后者为各种压缩格式。这里介绍一下位深与色深的概念:
①色深
色深指的是每一个像素点用多少bit来存储ARGB值,属于图片自身的一种属性。色深可以用来衡量一张图片的色彩处理能力(即色彩丰富程度)。
典型的色深是8-bit、16-bit、24-bit和32-bit等。
上述的Bitmap.Config参数的值指的就是色深。比如ARGB_8888方式的色深为32位,RGB_565方式的色深是16位。

②位深
位深指的是在对Bitmap进行压缩存储时存储每个像素所用的bit数,主要用于存储。由于是“压缩”存储,所以位深一般小于或等于色深 。
举个例子:某张图片100像素*100像素 色深32位(ARGB_8888),保存时位深度为24位,那么:
该图片在内存中所占大小为:100 * 100 * (32 / 8) Byte
在文件中所占大小为 100 * 100 * ( 24/ 8 ) * 压缩率 Byte

步骤

1、使用自己的手机截屏,保存图片,获取一张 1080 * 1920的图片,查看其sd卡路径 strPath。同时导入到AS 开发的app中 drawable目录下
在这里插入图片描述
位深24 这个位深只与图片在硬盘上占用空间有关,与加载到手机内存占用无关
2、获取bitmap getByteCount(),打印输出
图片放drawable目录

private void loadResImage() {
        BitmapFactory.Options options = new BitmapFactory.Options();
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.weixin, options);
        Log.i(TAG, "loadResImage: " + Environment.getExternalStorageDirectory());
        String str = Environment.getExternalStorageDirectory() + "/Pictures/Screenshots/Screenshot_20200221-110020.jpg";
        //Bitmap bitmap = BitmapFactory.decodeFile(str);
        //Log.i(TAG, "loadResImage: "+str);
        File file = new File(str);
        Log.i(TAG, "loadResImage: file:" + file);
        Log.i(TAG, "loadResImage: " + file.exists());
        //imageView.setImageBitmap(bitmap);
        DisplayMetrics metrics = new DisplayMetrics();
        WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(metrics);
        Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount()  + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
        Log.i(TAG, "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
        Log.i(TAG, "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
        //Log.i(TAG, "imageview.width:" + imageView.getWidth() + ":::imageview.height:" +    imageView.getHeight());
        Log.i(TAG, "loadResImage: options.inSampleSize:" + options.inSampleSize);
        Log.i(TAG, "loadResImage: " + metrics.density);
        Log.i(TAG, "loadResImage: " + metrics.heightPixels);
        Log.i(TAG, "loadResImage: " + metrics.widthPixels);
        Display display = getWindowManager().getDefaultDisplay();
        Point point = new Point();
        display.getSize(point);
        int width = point.x;
        int height = point.y;
        Log.i(TAG, "loadResImage: " + width);
        Log.i(TAG, "loadResImage: " + height);
    }

日志输出:
在这里插入图片描述
注意:为何打印屏幕分辨率时,本来是1920,却打印了1792,因为华为手机有虚拟导航栏,打开app前将 虚拟导航栏隐藏,再打开应用,就可以看到是1920了,计算时仍使用1920
计算:
使用公式1 图片内存占用大小:3240 * 5760 * 4 = 74649600 B = 72900 KB = 71.1914 MB
使用公式2 图片内存占用大小:1080 * (480 / 160)* 1920 * (480 / 160)* 4= 74649600 B = 72900 KB = 71.1914 MB

图片放SD卡
在这里插入图片描述
计算: 1080 * 1920 * 4 = 8294400 B

图片放xxhdpi ,因为 inDensity 和 inTargetDensity 相等,故图片不需要缩放,内存大小相当于从SD卡中加载
在这里插入图片描述
放xxxhdpi
在这里插入图片描述
为什么买的硬盘总会比标识的容量小那么一些呢?
硬盘生产厂商并不是以我们的1KB=1024byte去计算的,而是1KB=1000byte计算的,因此我们会觉得总是给少了。
参考:
Android Bitmap(位图)详解
Android中一张图片占据的内存大小是如何计算
Android高效内存1:一张图片占用多少内存
Android高效内存2:让图片占用尽可能少的内存
Android开发之高效加载Bitmap
Android: BitmapFactory.decodeResource BitmapFactory.decodeStream
Https://www.androidos.net.cn/sourcecode android 系统源码
BitmapFactory.decodeResource 和 BitmapFactory.decodeStream in Android区别
BitmapFactory.Options中的inDensity和inTargetDensity
BitmapFactory.Options中的inDensity,inTargetDensity,inScreenDensity详解
decode图片时BitmapFactory.Options中的inDensity和inTargetDensity
Android 屏幕真实分辨率获取
android BitMap回收
Android bitmap(三) BitmapFactory inSampleSize
Android中一张图片占用的内存大小
Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存? 从源码角度讲解
Bitmap知识点
Android Bitmap计算大小 getRowBytes和getByteCount()
字节换算(byte-to-bit)
b ,B,KB,MB,GB之间的关系
Bitmap的加载与缓存
Bitmap的加载和缓存


作者:王人冉


--结束END--

本文标题: Android中一张图片占用的内存大小

本文链接: https://www.lsjlt.com/news/28881.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作