一、前言
Bitmap在我们开发中是常用到的对象,比如列表图片的展示等等都需要用到Bitmap,而Bitmpa是内存使用的大客户。只不过我们在实际开发中用的都是比较成熟的框架Glide、ImageLoad等,这些框架对内存都设计了一定的优化方案。在介绍Bitmap的内存优化之前,我们先对Bitmap的使用做简单的介绍。
二、Bitmap的使用
我们首先来看下Bitmap的构造方法
/**
* Private constructor that must received an already allocated native bitmap
* int (pointer).
*/
// called from JNI
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}
mWidth = width;
mHeight = height;
mIsMutable = isMutable;
mRequestPremultiplied = requestPremultiplied;
mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}
mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
sPreloadTracingNumInstantiatedBitmaps++;
sPreloadTracingTotalBitmapsSize += nativeSize;
}
}
我们看到注释called from JNI。知道Bitmap的构造方法是停供给Native层调用的方法。所以我们知道Bitmap的创建涉及到底层库的支持。为了方便开发者,安卓中提供了BitmapFactory工具类来创建Bitmap对象,BitmapFactory中有一系列的decodeXXX方法读取资源文件,本地文件,流等方式。然后转换成输入流,接着调用native方法解析流,我们可以猜到解析流完之后,再调用Bitmap的构造方法,创建Bitmap 对象。
我们来梳理下Bitmap的创建流程:
1、通过BitmapFactory调用decodeXXX方法读取文件,流等。
2、调用naitive方法,解析流。
3、naitive调用Bitmap的构造方法创建Bitmap对象
我们来看下BitmapFactory中的不decodeXXX方法
// BitmapFactory部分代码:
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeStream(InputStream is)
private static native Bitmap nativeDecodeStream
我们可以看到nativeDecodeStream就是属于native方法,本地方法我们看不到具体实现,但是我们可以猜到是通过该方法解析流。
我们先来看下BitmapFacotry的一个简单的使用,加载本地的一个图片。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.image_my_bugt);
ivHead.setImageBitmap(bitmap);
当然我们也可以如下操作:
ivHead.setImageResource(R.drawable.image_my_bugt);
其实效果是一样的,setImageResource只是封装了Bitmap 的读入和解析的过程。这个过程都是在UI线程中执行的,对性能是有所影响的。
三、Bitmap到底占多大内存
下面我们使用一个分辨率为750*594的图片,大小为453KB作为例子。我们把它放到drawable目录下。
我们将如下信息打印
E/MainActivity: ----Width: 2250
E/MainActivity: ----Height: 1782
E/MainActivity: ----Config: ARGB_8888
E/MainActivity: ----mDensity: 480
E/MainActivity: ----size: 16038000
我们可以看到分辨率为750*594的图片,宽和高放大三倍,而原来的内存是453KB,在系统中占的内存为16038000个字节,16M左右。对内存的开销是非常大的。
为什么原图和打印出的信息有这么大的差距呢。其实在naitive方法中可以提现出来,Bitmap最终是被绘制出来的,而绘制之前有如下操作
scale = (float) targetDensity / density;
scale 就是缩放倍数。这个时候 我们知道 缩放的倍数和targetDensity 以及density有关,他们分别代表什么呢?
targetDensity :Bitmap最终绘制的目标位置分辨率,这个和设备的分辨率保持一致。
inDensity:位图自身的密度,分辨率。这个很关键。因为图片放在不同的drawble目录下,分辨率会不一样。通常如下图:
我们通过scale = (float) targetDensity / density公式知道,位图自身的分辨率越小,那么缩放就越小,在内存中占用内存就越小。但是如果缩放超出了自身的大小,图片就变形,变模糊了。因此我们得出的结论是最好图片放在和自身分辨率接近的目录下。
比如 红米4,Android 6.0系统,设备dpi 480。我们的图片放在xxhdpi目录下的时候刚好是480,这样缩放的大小刚好是1倍,也就是原图。这样是最佳的选择方案,既不会导致内存过大的开销,同时也保存最原始的图片。
我们小结一下:
(1)同一张图片,放在不同资源目录下,其分辨率会有变化,
(2)bitmap分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少
(3)图片不特别放置任何资源目录时,其默认使用mdpi分辨率:160
(4)资源目录分辨率和设备分辨率一致时,图片尺寸不会缩放
3、Bitmap的内存优化
关于Bitmap的内存公式可以细化为:
Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (设备分辨率/资源目录分辨率)^2 × 每个像素的字节大小
我们把以上的数据套进公式就可以得出结论
关于Bitmap内存的优化方案:
(1)使用低色彩的解析模式,如RGB565,减少单个像素的字节大小
(2)资源文件合理放置,高分辨率的图片可以放到高分辨率目录下。最好是位图自身的分辨率和手机设备的分辨率接近或者一样。
(3)对图片进行缩小
(4)图片内存的复用,缓存
4、小结
1、Bitmap的创建通过naitive方法解析流,然后进行绘制,最后调用Bitmap的构造方法,创建Bitmap对象
2、naitive方法绘制的时候先对图片进行缩放操作,缩放公式scale = (float) targetDensity / density;,缩放大小和目标绘制分辨率以及自身分辨率有关
3、不同的drawble目录下位图自身的密度、分辨率不一样。即density不一样。
4、建议高分辨率的图片放在高分辨率的drawble分辨率下,反之也一样
5、一般实际的图片大小大于手机上需要显示的大小,可以对原图进行缩小,节省内存
6、相同的图片可以复用,缓存。节省内存的开支。