BitmapFactory 加载图片到内存

时间:2023-03-09 09:05:33
BitmapFactory 加载图片到内存

Bitmap占用内存分析
Android的虚拟机是基于寄存器的Dalvik,它的最大堆(单个进程可用内存)大小一般是16M,当然不同设备是不一样的,可以查看/system/build.prop文件,[注:现在的机子可能一般都有160M左右]。程序本身运行就占有一定的内存,在使用较大的bitmap时,由于Bitmap自身的特性(将每个像素的属性全部保存在内存中),导致稍有不慎就会创建出一个占用内存非常大的Bitmap对象,从而导致加载过慢,还会有内存溢出的风险。所以,加载Bitmap时对其进行优化是必不可少的一步。
例如:
mImageView.setImageResource(R.drawable.my_image);
这是一行从资源文件中加载图片到ImageView的代码。实际上,以上这行代码会在运行时使用BitmapFactory.decodeStream()方法将资源图片生成一个Bitmap,然后由这个Bitmap生成一个Drawable,最后再将这个Drawable设置到ImageView。由于在过程中生成了Bitmap,因此如果你使用的图片过大,就会导致性能和内存占用的问题。


图片的不同存在形式
  • 文件形式,即以二进制形式存在于硬盘上,file.length()
  • 流的形式,即以二进制形式存在于内存中,将文件读取到流中时,流的大小和文件在磁盘中显示的大小是一样的
  • Bitmap形式,即以RGBA(默认)形式存在于内存中,将文件或流中的数据decode为bitmap时,占用的内存会瞬间变的超大(能大上百倍),bitmap.getByteCount()
之所以文件形式占用的磁盘空间比内存形式的Bitmap小很多,主要是图片被有损压缩了,若不对图片进行任何压缩处理,则两者的占用的大小应该是一致的。

Bitmap占用的内存
计算公式:占用内存大小= Bitmap宽度 * 高度 * 每个像素占用的内存 = 像素总数 * 每个像素占用的内存
在Android中,Bitmap有四种像素类型:ARGB_8888、ARGB_4444、ARGB_565、ALPHA_8,他们每个像素占用的字节数分别为4、2、2、1。因此,一个2000*1000的ARGB_8888类型的Bitmap占用的内存为2000*1000*4=8000000B=8MB。
注意:Bitmap占用内存大小和磁盘中的.png图片占用磁盘空间的大小可以说没有一毛钱关系,但和在磁盘中显示的图片的宽高有决定性关系!

系统分配内存
Android根据设备屏幕尺寸和dpi的不同,给系统分配的单应用程序内存大小也不同,具体如下表(表格取自Android 4.4 Compatibility Definition Document (CDD)):
屏幕尺寸	                 DPI	                 应用内存
small / normal / large	 ldpi / mdpi	 16MB
small / normal / large	 tvdpi / hdpi	 32MB
small / normal / large	 xhdpi	         64MB
small / normal / large	 400dpi	         96MB
small / normal / large	 xxhdpi	         128MB
xlarge	                         mdpi	         32MB
xlarge	                         tvdpi / hdpi	 64MB
xlarge	                         xhdpi	         128MB
xlarge	                         400dpi	         192MB
xlarge	                         xxhdpi	         256MB

实际测试
在720P的华为荣耀2和1080P的华为荣耀7上都能正常加载 5016*7891*4/1024/1024 = 150M 的图片
Log.i("bqt", bitmap.getWidth() * bitmap.getHeight() * 4 / 1024 / 1024 + "-" + bitmap.getByteCount() / 1024 / 1024);//150-150
而对于 5612*8414*4/1024/1024 = 180M 的图片都加载失败了,但应用并没有挂掉,所以估计单个应用的内存被调到了160M。
BitmapFactory 加载图片到内存
注意区分占用内存(宽*高*每个像素占用的内存)和图片像素(宽*高)的区别
对于ARGB_8888来说,内存=像素*4
所以1000万像素占用内存近似为:10M*4=40M

Bitmap.createBitmap方法
1、创建一个空白的位图。此位图是mutable(可修改的)
  • public static Bitmap createBitmap(int width, int height, Config config)
  • public static Bitmap createBitmap(DisplayMetrics display, int width, int height, Config config)
Confg 图像格式(实际上就是颜色模式),有以下几种
  • ALPHA_8:每个像素占用1byte内存(8位),灰白照片,只有256位的灰度
  • ARGB_4444:每个像素占用2byte内存(16位,废弃了)
  • RGB_565:每个像素占用2byte内存(16位)
  • ARGB_8888:每个像素占用4byte内存(32位)带透明度,是Android默认的颜色模式,这个颜色模式色彩最细腻,显示质量最高,但占用的内存也最大。如一张720P的图片占用内存大小为720*1280*4/1024/1024=3.5M。
注意:Bitmap占用内存大小和硬盘中的.png图片的大小没有一毛钱关系!

2、根据一个已存在的位图创建新的位图。此位图是immutable(不可修改的)
  • public static Bitmap createBitmap(Bitmap src) 从原位图src复制出一个新的位图,和原始位图完全相同
  • public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)
  • public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)
从原始位图指定位置开始剪切一个指定大小的图像,并且可以使用Matrix来实现旋转等效果的高级截图方式。
参数说明:
int x/y 起始x/y坐标;int width/height 要截的图的宽度/高度;Matrix m 通过此矩阵对原图进行操作;
boolean filter :当进行的不只是平移变换时,filter参数为true可以进行滤波处理,有助于改善新图像质量;flase时,计算机不做过滤处理。

3、根据指定的颜色数组创建位图。此位图是immutable(不可修改的)
  • public static Bitmap createBitmap(DisplayMetrics display, int[] colors, int width, int height, Config config)
  • public static Bitmap createBitmap(DisplayMetrics display, int[] colors, int offset, int stride, int width, int height, Config config)
  • public static Bitmap createBitmap(int[] colors, int width, int height, Config config)
  • public static Bitmap createBitmap(int[] colors, int offset, int stride, int width, int height, Config config)
根据颜色数组来创建位图。注意:颜色数组的长度一定要>=width*height,否则抛出异常。
此函数创建位图的过程可以简单概括为:根据width和height创建空位图,然后用指定的颜色数组colors来从左到右从上至下依次填充颜色。


BitmapFactory.decode***方法
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeFile(String pathName, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts)
public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)

以上所有重载方法,参数较少的方法都是直接调用参数较多的方法,调用时传入的其他参数为null。
以上方法返回的Bitamp都是不可修改的。
如果指定文件或流或数组为空或者不能解码成Bitmap,或者opts参数只请求大小信息时,返回NULL。

BitmapFactory.Options简介
boolean inJustDecodeBounds——如果设置为true,不获取图片,不分配内存,但会返回图片的宽高信息
int outWidth——获取图片的宽度值 int outHeight——获取图片的高度值
int inSampleSize——图片缩放的倍数。如果设为4,则宽和高都为原来的1/4,则图是原来的1/16 int inDensity——用于位图的像素压缩比 int inTargetDensity——用于目标位图的像素压缩比(要生成的位图) boolean inScaled——设置为true时进行图片压缩,从inDensity到inTargetDensity
boolean inPurgeable——设置图片是否可以被回收,创建Bitmap时用于存储像素的内存空间在系统内存不足时可以被回收
boolean inInputShareable ——设置是否解码位图的尺寸信息
boolean inDither——设置是否进行图片抖动处理
Bitmap.Config inPreferredConfig——设置颜色格式,设为null可让解码器以最佳方式解码

关于inSampleSize
该参数为int型,他的值指示了在解析图片为Bitmap时在长宽两个方向上像素缩小的倍数。inSampleSize的默认值和最小值为1,当小于1时,解码器将该值当做1来处理,且在大于1时,该值只能为2的幂(当不为2的幂时,解码器会取与该值最接近的2的幂)。例如,当inSampleSize为2时,一个2000*1000的图片,将被缩小为1000*500,相应地,它的像素数和内存占用都被缩小为了原来的1/4:

加载大图详细demo
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView imageView = new ImageView(this);
        //通过设置不同的显示模式,可以观察压缩后的Bitmap到底多大
        imageView.setScaleType(ScaleType.CENTER);//按图片原来大小居中显示,若图片长/宽 > View的长/宽,则截取图片的居中部分显示
        //imageView.setScaleType(ScaleType.CENTER_INSIDE);//将图片完整居中显示,通过按比例缩小使得图片长/宽 <= View的长/宽

        //1.得到加载Bitmap的View(屏幕)的宽高,API13 android3.2 之后才能用
        Point point = new Point();
        getWindowManager().getDefaultDisplay().getSize(point);
        int screenWidth = point.x;
        int screenHeight = point.y;
        Log.i("bqt", "屏幕宽高:" + screenWidth + "-" + screenHeight);//屏幕宽高:1080-1794

        //2.得到原始图片的宽高
        BitmapFactory.Options options = new Options();//加载和显示图片是很消耗内存的,Options 类允许我们定义图片以何种方式读到内存
        options.inJustDecodeBounds = true;//不去解析真实的位图,只是获取这个位图的边界等信息。这是关键!
        Bitmap bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + "/big_image.jpg", options);
        int bitmapWidth = options.outWidth;
        int bitmapHeight = options.outHeight;
        Log.i("bqt", "图片宽高:" + bitmapWidth + "-" + bitmapHeight + "-" + (null == bitmap));//3600-5400-true。返回的是null

        //3.计算缩放比例。这里为了后面的演示将scale设为了float,实际中全部使用int计算即可,因为后面用到的也是int
        float scaleX = 1.0f * bitmapWidth / screenWidth;
        float scaleY = 1.0f * bitmapHeight / screenHeight;
        float scale = Math.max(scaleX, scaleY);//以最大限度显示图片
        Log.i("bqt", "缩放前:" + scaleX + "-" + scaleY + "-" + scale);//3.3333333-3.0100334-3.3333333

        //4.缩放加载图片到内存
        //4.1.要想节约内存,必须使用 inSampleSize 这个成员变量
        //options.inSampleSize = (int) scale;//此值最终会被解析器解析成2的n次幂(1、2、4、8),表示图片宽高缩小到原来的几分之一
        options.inSampleSize =calculateInSampleSize(options, screenWidth, screenHeight);
        //4.2.使用下面两个成员变量并不会改变加载进内存的Bitmap的大小,所以根本用不着
        options.outWidth = (int) (bitmapWidth / scale);
        options.outHeight = (int) (bitmapHeight / scale);
        Log.i("bqt", "options中设置的宽高:" + options.outWidth + "-" + options.outHeight + "-" + options.inSampleSize);//1080-1620-3
        //4.3.下面这些参数都不是必须的
        options.inPurgeable = true;//建议加上。设置图片可以被回收,创建Bitmap时用于存储像素的内存空间在系统内存不足时可以被回收
        options.inInputShareable = true;//建议加上。设置解码位图的尺寸信息
        options.inDither = false; //不进行图片抖动处理
        options.inPreferredConfig = null; //让解码器以最佳方式解码
        //4.4.真正的去解析这个位图。
        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + "/big_image.jpg", options);
        Log.i("bqt", "实际bitmap的宽高:" + bitmap.getWidth() + "-" + bitmap.getHeight() + "-" + options.inSampleSize);//1800-2700-3
        //注意,实际bitmap的宽高既不是我们设置的options.outWidth=1080,也不是bitmapWidth/(int) scale=1200,而是bitmapWidth/2=1800
        imageView.setImageBitmap(bitmap);
        setContentView(imageView);
    }

    /**在保证解析出的bitmap宽高分别大于目标尺寸宽高的前提下,取可能的inSampleSize的最大值*/
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // 原始图片的宽高
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            int halfHeight = height / 2;
            int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

加载大图简洁demo
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView imageView = new ImageView(this);
        Bitmap bitmap = decodeSampledBitmapFromFile(Environment.getExternalStorageDirectory().getPath() + "/a.jpg", 1080, 1920);
        imageView.setImageBitmap(bitmap);
        Log.i("bqt", "加载进内存的bitmap的宽高:" + bitmap.getWidth() + "-" + bitmap.getHeight());//1800-2700
        setContentView(imageView);
    }
    public static Bitmap decodeSampledBitmapFromFile(String pathName, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(pathName, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(pathName, options);
    }
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int width = options.outWidth;
        int height = options.outHeight;
        Log.i("bqt", "原始图片的宽高:" + width + "-" + height);//3600-5400
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            int halfHeight = height / 2;
            int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}