在开发app时,显示一张本地图片,这张图片在加载时会占用大多内存呢?猜测占用内存大小和以下几个因素有关:
- 设计师切图,图片本身的分辨率;
- 图片所放文件夹代表的 密度 dpi;
- 手机自身的屏幕密度;
- 经过系统缩放得到的最终加载到手机上图片的密度和占用的内存。
我们知道Android中在加载本地大图时,很容易OOM,主要原因在于加载的Bitmap占用内存太大。接下来将围绕以下几个问题说明如何计算一张Bitmap占用的内存大小。
- 将一张分辨率为 720x1080 的图片放到 xxhdpi 或者 hdpi ,同放在 xhdpi 标准文件夹下,对于同一台手机占用内存大小是否有变化?
- 同一张分辨率为 720x1080 的图片被不同屏幕分辨率的手机加载,BitmapFactory 的成员变量 inDensity、 inScreenDensity、 inTargetDensity 会怎样变化?这些值又是怎样被赋值的,又是怎样进行缩放的?
- 使用 decodeResource() 和 decodeStream() 有什么区别?
- Options 的 inDensity、 inTargetDensity 和 输出的 Bitmap 的 mDensity 有什么关系?Bitmap 的 mWidth、 mHeight 与 Options 的 outputWidth、 outputHeight 有什么关系?
- 这些同计算 Bitmap 内存占用大小的 长宽有什么关系?
在回答这些问题之前,先介绍一下DisplayMetrics和Bitmap及其相关类。
一、DisplayMetrics和Bitmap及其相关类
DisplayMetrics
说明:屏幕密度相关类,可以用于获取屏幕高和宽以及屏幕密度density、每英寸点数densityDpi . 这里,density 数值为 1dp = density px;在 DisplayMetrics 中,这两个是线性相关:
屏幕密度对照表.png
Bitmap
说明:Bitmap 在 Android 中指的是一张图片,可以是 png,也可以是 jpg等其他图片格式。
作用:可以获取图像文件信息,对图像进行剪切、旋转、缩放、压缩等操作,并可以指定格式保存图像文件。
Bitmap.Config
说明:Bitmap 格式。除了尺寸外,影响一个图片占用空间还有色彩细节。位图位数越高表示可以存储的颜色信息越多,图像也就越清晰逼真。
- ALPHA_8:表示8位Alpha位图,每像素占1byte内存;
- RGB_565:表示R为5位,G为6位,B为5位,一共16位,每像素占2byte内存;
- ARGB_4444:表示16位位图,每像素占2byte内存(poor quality - Android Deprecated);
- ARGB_8888:表示32位ARGB位图,每像素占4byte内存(Recommended)。
BitmapFactory
说明:提供解析Bitmap的静态工厂方法。
BitmapFactory.Options
说明:用于解码Bitmap时的各种参数控制。
几个重要参数:
inBitmap:在解析Bitmap时重用该Bitmap,但是必须相同大小的Bitmap & inMutable = true 才可重用;
inMutable :配置Bitmap是否可更改,如每隔几个像素给Bmp添加一条直线;
inPreferredConfig:Config颜色位数,默认值为Bitmap.Config.ARGB_888;
inDither:是否抖动,默认false(Android Depracated);
inPremultiplied:默认true,一般不改变其值。
inPurgeable:当存储像素内存空间 在系统内存不足时 是否可被回收(Android L Deprecated);
inInputShareable:是否可以共享一个 InputStream (Android L Deprecated);
inPreferQualityOverSpeed:为true时会优先保证 Bitmap 质量,其次是解码速度(Android N Deprecated);
inTempStorage:解码时的临时空间,建议 16K;
inJustDecodeBounds:为true时仅返回 Bitmap 宽高等属性,返回bmp=null,为false时才返回占内存的 bmp;
inSampleSize:表示 Bitmap 的压缩比例,值必须 > 1 & 是2的幂次方。inSampleSize = 2 时,表示压缩宽高各1/2,最后返回原始图1/4大小的Bitmap;
inDensity:表示 Bitmap 像素密度;
inTargetDensity:表示 Bitmap 最终的像素密度;
inScreenDensity:表示当前屏幕的像素密度;
inScaled:默认为true,是否支持缩放,设置为true时,Bitmap将以 inTargetDensity 的值进行缩放;
outputWidth:返回的 Bitmap的宽;
outputHeight:返回的 Bitmap的高。
以一张类图说明Bitmap、BitmapFactory和BitmapFactory.Options三者之间的关系,如下图所示:
Bitmap、BitmapFactory、Options关系类图.png
二、ImageView 设置图片 & Bitmap创建流程
ImageView 设置图片
一般地,给 ImageView 设置资源图片时,会用到四种方式:setImageResource(), setImageUri(), setImageBitmap(), setImageDrawable。这四种方式有什么区别呢?用一张图来展示:
总结:由上可知,ImageView设置本地图片会先生成 Bitmap 再将 Bitmap 转成 Drawable,最终通过 setImageDrawable() 设置;
【所以这步是否可以看做使用 setImageDrawable 会跳过读取和解码 Bitmap 操作,为最优设置本地图片方式呢?
—— 需测试内存占用情况方可验证。】
Bitmap创建流程
BitmapFactory 提供了五种方式来创建Bitmap,分别是:decodeFile, decodeResource, decodeByteArray, decodeStream, decodeFileDescription,这里只介绍常见三种方式创建流程如下:
总结:
- 最常用的三个方法:decodeFile, decodeResource, decodeStream,前两个最终调用的是 decodeStream;
- **decodeStream, decodeByteArray, decodeFileDescription **这三个内部则调用的是 native 方法来创建 Bitmap的【有种说法,Bitmap是Android中唯一通过 native 方法创建的类】;
- decodeResourceStream主要做了两件事:一是对 opts.inDensity 赋值,没有设置默认值 160;二是对 opts.inTargetDensity 赋值,没有赋值为当前设备 densityDpi;
- decodeStream主要也做了两件事:一是调用 native 方法解析 Bitmap;二是对解析得到的 Bitmap 调用 setDensityFraomOptions(bmp, opts) 进行设置;
- setDensityFraomOptions(bmp, opts)主要做了这样几件事:一是当opts.inDensity != opts.inTargetDensity || opts.inDensity != opts.inScreenDensity && (inScaled = true || isNinePatch) 时,将设置 outputBitmap.mDensity = inTargetDensity;
decodeResourceStream()方法源码如下:
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
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);
}
setDensityFromOptions(bmp, opts)源码如下:
private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
if (outputBitmap == null || opts == null) return;
final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}
byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled || isNinePatch) {
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}
三、如何计算Bitmap占用内存大小?
常规方式:
API方法:getByteCount() 获取 - 不准确
粗略方式:
计算公式:图片长 * 宽 * 4bytes/ARG_8888 - 不正确
通读源码得来的方式:
/**
* Returns the minimum number of bytes that can be used to store this bitmap's pixels.
*
* <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
* no longer be used to determine memory usage of a bitmap. See {@link
* #getAllocationByteCount()}.</p>
*/
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
/**
* Return the number of bytes between rows in the bitmap's pixels. Note that
* this refers to the pixels as stored natively by the bitmap. If you call
* getPixels() or setPixels(), then the pixels are uniformly treated as
* 32bit values, packed according to the Color class.
*
* <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, this method
* should not be used to calculate the memory usage of the bitmap. Instead,
* see {@link #getAllocationByteCount()}.
*
* @return number of bytes between rows of the native bitmap pixels.
*/
public final int getRowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mNativePtr);
}
最终通过native源码方法,可得到:一张ARGB_8888 的Bitmap占用内存计算公式:bmpWidth * bmpHeight * 4byte。不是直接使用图片分辨率进行计算,而是界面后 Bitmap 的宽高进行计算。
然而,这样计算并不准确。有几个不同的场景会导致最终计算的结果不正确。
- 将一张 720x1080 图片分别放在不同分辨率drawable文件夹下,在同一个手机上加载;
- 也是同一张图片放在指定分辨率的 drawable 文件夹下,在不同手机上加载;
- 切不同分辨率图片到对应 drawable 文件夹下,在各分辨率设备上加载。
一般,我们读取 drawable 目录下的图片,会用到 <code>decodeResource</code>获取 Bitmap,该方法可以直接看上面提到的 decodeResourceStream() 方法源码,通过源码可知:
- 在读取资源时,使用 openRawResource 方法,然后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息,也即是文件夹代表的density;
- 调用 decodeResourceStream 对原始资源进行解码和适配,实际是原始资源 density 到 设备屏幕 density 的映射。
这里看一下 资源文件夹代表的密度:
资源文件夹密度对照表.png
对照 decodeResourceStream() 源码如何设置 opts.inDensity 逻辑:
资源解码Bitmap参数设置流程.png
最后通过查阅 native 源码,得到计算公式:
一张图片对应 Bitmap 占用内存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);
Native 方法中,mBitmapWidth = mOriginalWidth * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize,
mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize
现在针对介绍的几种场景,会得到这样的结论:
- 将一张 720x1080图片放在 drawable-xhdpi 目录下(inDensity = 320),
- 在 720x1080 手机上加载(inTargetDensity = 320),图片不会被压缩;
- 在 480x800 手机上加载(inTargetDensity = 240),图片会被压缩 9/16;
- 在 1080x1920 手机上加载(inTargetDensity = 480),图片会被放大 2.25;
- 切不通分辨率大小的图片放到对应文件夹下,会根据屏幕获取对应文件夹的图片,就不存在加载图片时压缩和放大(针对标准屏);
拓展问题:只切一套UI图,是否适用?如何选择?
注意,上述计算方式是在通过 decodeResource() 方法获取 Bitmap 的情况下得出,其他几种方式获取Bitmap,最后得到占用内存Size不会跟资源文件目录相关联。
四、问题解答
问题一:一张图片对应 Bitmap 占用内存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);Native 方法中,mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize ;
由此可知,手机屏幕大小 1280 x 720(inTarget = 320),加载 xxhdpi (inDensity = 480)中的图片 1920 x 1080,scale = 320 / 480,inSampleSize = 1,最终获得的 Bitmap 的图像大小是 :
mBitmapWidth = opts.outWidth = 1080 * (320 / 480) * 1/1 = 720,
mBitmapHeight = opt.outHeight = 1920 * (320 / 480) * 1/1 = 1280,
getAllocatedMemory() = mBitmapWidth * mBitmapHeight * 4 = Bitmap占用内存。
问题三:使用 decodeResource() 和 decodeStream() 有什么区别?
(1)decodeResource() 流程,会先用 TypedValue 保存图片信息,然后会根据条件设置 opts.inDensity = value.inDensity,为0则设置为默认 160dpi; 文件夹代表密度
Opts.inTargetDensity = getDisplayMetrics().densityDpi; 屏幕密度
设置完上述参数后,最终还是会调用 decodeStream() 方法;
(2)decodeStream() native 方法得到 Bitmap后,调用 setDensityFromOptions() 方法来设置 Bitmap.mDensity:
若 opts.inDensity != 0,bitmap.mDensity = opts.inDensity;
若 opts.inTargetDensity != 0 && inDensity != targetDensity && inDensity != screenDensity,继续判断,如果 opts.inScaled || isNinePatch,bitmap.mDensity = targetDensity;
所以,
(1)若使用 decodeResource() 加载本地图片,inDensity 为加载图片所在的文件夹代表的 dpi,inTargetDensity 为目标屏幕密度(or 图片真实像素密度?),
最终 bitmap.mDensity = targetDensity。
(2)若使用 decodeStream() 则不会先记录图片信息,得到bitmap 后,直接调用 setDensityFromOptions() 方法,所以最终 bitmap.mDensity = defaultDensity() = DENSITY_DEVICE。
参考源码API-26
参考:http://dev.qq.com/topic/591d61f56793d26660901b4e
https://www.tuicool.com/articles/3eMNr2n
如有误,请指正!
网友评论
但是如果dpi的文件夹和屏幕分辨率不对等,首先需要把图片转换城对应屏幕分辨率的dpi,因此同一图片放在不同dpi的文件夹下在同一设备上加载的内存是不一样的;计算方式是:图宽/图屏dpi比*图高/图屏dpi比*字节。因此在同一个设备上,从占用内存上来讲图片的DPI应当大于设备dpi的;但是从应用成像的清晰度和主流设备的情况考虑,应该让设计师针对主流机型分辨率的设计稿进行切图( xhdpi / xxhdpi),但是有部分机型会大于xxhdpi,为了开发方便减少包的大小,直接舍弃xhdpi,只适配xxhdpi和xxxhdpi