Android中一张图片占用的内存大小
最近面试过程中发现对Android中一些知识有些模棱两可,之前总是看别人的总结,自己没去实践过,这两天对个别问题进行专门研究
探讨:如何计算Android中一张图片占据内存的大小解释:此处说的占据内存,是APP加载图片用的内存,即APP运行时图片占用的内存,不考虑图片显示到界面占用的内存(即只加载图片,不显示。因为显示图片时,系统有可能根据控件的大小或其他因素对内存进行了优化,不确定因素太多),同时也不考虑使用三方库加载图片占用的内存,如glide、等三方图片库,三方库在加载图片的过程中有可能对图片进行了优化,会<=图片加载占用的内存。
将通过BitmapFactory加载的方式,获取bitmap的字节数,来计算图片占用的内存大小。建议,为了避免因系统对bitmap对象的复用(有可能)导致的bitmap字节数的偏差,每次获取对象大小前,最好杀死应用,从新打开App。
本片博客讲诉的内容,有不正确的地方,欢迎大家斧正。由于很多博客都已经讲过这个问题,这里只做基本知识梳理、补充。
占用空间的大小不是图片占用内存的大小,占用空间是在磁盘(电脑硬盘或者手机SD卡)上占用的空间,内存大小是加载到内存中占用的内存大小。两个只是单位是一样的,但不是一个概念,不要混淆。
这里所说的图片分两类:
所得结论:
公式1:
或公式2:
图片占用的内存 = 原图宽 * (inTargetDensity / inDensity) * 原图高 * (inTargetDensity / inDensity) * 每个像素占用的字节数即图片所有像素点占用的字节数
如果 inTargetDensity / inDensity > 1则,加载到内存 图片被放大,否则被缩小
注意 :如果 BitmapFactory.Options options = new BitmapFactory.Options(); 中 options.inSampleSize != 0 ,则以上公式中。默认options.inSampleSize为0,源码中关于此属性有一段注释,如果为0,则nativate层按1来取值
inSampleSize > 1时
(注意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
代码里指 inDensity
目录名称与 inDensity 的对应关系如下
drawable 没带后缀对应的inDensity为 160 dpi
将一张图片放入不同的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时
(注意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的加载和缓存
作者:王人冉
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341