在Android设备中,内存的分配是有限的,每个APP分配一定的内存空间,当内存使用达到一定的阈值,就会触发GC,当内存超过最大内存,就会OOM然后就凉凉。因此,内存是相当珍贵的,一个流畅的APP需要做好内存优化。而图片,尤其是大图,是特别消耗内存的,江湖人称“内存杀手”。
一张图片到底占用多少内存
在计算图片占用内存之前,需要厘清两个概念。
-
图片占用内存大小
图片占用内存大小不等同于图片占用磁盘空间大小,图片在存储的时候会经过图片压缩算法进行压缩,然后保存成jpg、png等格式。因此一般情况下,图片占用磁盘空间大小都要小于图片占用内存大小。就像一张充气凳子,不用的时候放气折叠起来放着,使用的时候充气恢复原来的样子。
图片占用内存的大小 = 宽*高*单个像素占用的字节数
宽高指的是图片加载到内存中的宽高,从不同的drawable文件夹加载的图片在不同分辨率的手机上显示会对图片的宽高进行相应的缩放,这是屏幕适配的基本知识,这里不进行深入。
-
单个像素占用的字节数
一个像素占用多大内存空间是由图片加载的模式决定的,Android中默认使用ARGB_8888的模式加载图片,即一个像素点占用2byte。这里简单介绍一下各个模式:
- ALPHA_8:像素点只有alpha通道,1个像素点占用1byte。这个模式下只有透明度,一般用来做遮罩层。
- RGB_565:R通道占用5bit,G通道占用6bit,B通道占用5bit,总共占用2byte。这个模式没有ALPHA通道,对于色彩要求不高的情况,可以采用RGB_565,Glide默认使用这个模式。
- ARGB_4444:ARGB四个通道分别占用4bit,总共占用2byte。色彩表现不好,已经被废弃了,在4.4之后使用ARGB_4444会默认使用ARGB_8888替代。
- ARGB_8888:ARGB四个通道分别占用8bit,总共占用4byte。比较灵活,色彩表现度较好,是推荐使用的模式,也是默认使用的模式。
- RGBA_F16:一个像素占用8byte,适用于广色域显示。实际开发中暂时还没见到用这个的场景。
在理解上述概念的前提下,基本上对于一张图片占用多大内存可以心中有数。这里尝试加载一张图片进行验证,图片的信息如下图,是一张690*12287的大图。
按照上述方法计算结果为:占用内存的大小 = 690 * 12287 * 4 = 33972120。
通过Android Profiler导出内存进行验证,和计算结果是吻合的。吻合是吻合,但是大约占了34M的内存,显示效果如下。
花了34M显示出这个效果,心疼的抱住了胖胖的自己。
拿大图怎么办
那么遇到大图怎么办呢。分成两种情况,一种是我就想把这么大图片设置到比较小的ImageView上,比如把200px*200px的图片加载到100px*100px的ImageView上,从上图可以看出来,大图设置到小的控件上,一样看不清楚,因此只需要把图片缩放成100*100的就可以了。另一种是上面这种超过屏幕大小的大图,ImageView没办法设置那么大,但是又想看高清大图,就可以采用局部加载的方式。
图片采样率缩放
图片采样率缩放,是通过对图片进行一定比例的缩放,减少图片的像素点,进而减少图片占用内存的大小。主要通过BitmapFactory.Options类实现。
BitmapFactory.Options主要属性如下:
- inJustDecodeBounds:该属性设置为true时,只加载图片信息到Options中,而不将图片加载到内存中。
- outWidth:图片的宽度。
- outHeight:图片的高度。
- inSampleSize:采样率,如果值大于1,将对图片进行二次采样,宽和高分别按相应的比例缩小。例如设置为2,则宽和高缩小为原来的2分之一,即200*200的图片缩小成100*100,图片占用的内存则为原来的4分之一。inSampleSize应该设置为2的n次方,否则向下取最近的2的n次方数。比如设置inSampleSize=10,则实际inSampleSize向下取2的3次方为8,因此缩小成8分之一。
具体实现代码如下:
BitmapFactory.Options options = new BitmapFactory.Options();
//只读取图片信息
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.ad, options);
//读取图片宽和高
int imgWidth = options.outWidth;
int imgHeight = options.outHeight;
//读取ImageView宽和高
int ivHeight = imageView.getHeight();
int ivWidth = imageView.getWidth();
//分别计算宽和高的比值
int scaleX = imgWidth / ivWidth;
int scaleY = imgHeight / ivHeight;
int inSampleSize = 1;
//取较大的值作为采样率
inSampleSize = scaleX > scaleY ? scaleX : scaleY;
//防止采样率小于1
if (inSampleSize < 1)
inSampleSize = 1;
Log.i("bitmap", "inSampleSize = " + inSampleSize);
//根据采样率加载图片
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ad, options);
imageView.setImageBitmap(bitmap);
这个方式加载出来的效果和上面的一样,就不多放一张图片了。而这时候图片会占用多少内存呢?这里计算图片内存的时候需要注意,当宽/采样率不能整除的情况下,采用进一法计算,比如该图片宽690,计算出来inSampleSize为10,根据前面的描述可以知道实际inSampleSize为8,690/8=86.25,因此最后宽为87。高为12287/8=1535.875,进一法得1536。因此图片占用内存大小为87*1536*4=534528。
同样dump出内存进行验证。
按区域加载
按区域加载通过BitmapRegionDecoder类实现,用到的参数主要是Rect和BitmapFactory.Options,其中Rect指定加载图片的区域。
实现代码如下:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.ad, options);
//老套路,计算图片的宽和高
int imgWidth = options.outWidth;
int imgHeight = options.outHeight;
InputStream is = getResources().openRawResource(R.raw.ad);
//初始化BitmapRegionDecoder
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
//设置图片加载模式为RGB_565
options.inPreferredConfig = Bitmap.Config.RGB_565;
//加载图片中心位置300px*300px大小的图片
Bitmap bitmap = decoder.decodeRegion(new Rect(imgWidth/2 - 150, imgHeight/2 - 150, imgWidth/2 + 150, imgHeight/2 + 150), options);
imageView.setImageBitmap(bitmap);
显示效果如下: