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。
注意区分占用内存(宽*高*每个像素占用的内存)和图片像素(宽*高)的区别
对于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;
    }
}

BitmapFactory 加载图片到内存的更多相关文章

  1. 图片--Android加载图片导致内存溢出(Out of Memory异常)

    Android在加载大背景图或者大量图片时,经常导致内存溢出(Out of Memory  Error),本文根据我处理这些问题的经历及其它开发者的经验,整理解决方案如下(部分代码及文字出处无法考证) ...

  2. Android加载图片导致内存溢出(Out of Memory异常)

    Android在加载大背景图或者大量图片时,经常导致内存溢出(Out of Memory  Error),本文根据我处理这些问题的经历及其它开发者的经验,整理解决方案如下(部分代码及文字出处无法考证) ...

  3. [Android]异步加载图片,内存缓存,文件缓存,imageview显示图片时增加淡入淡出动画

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/3574131.html  这个可以实现ImageView异步加载 ...

  4. android 加载图片防止内存溢出

    图片资源: private int fore[]; private int back[]; fore = new int[]{R.drawable.a0, R.drawable.a1, R.drawa ...

  5. WPF循环加载图片导致内存溢出的解决办法

    程序场景:一系列的图片,从第一张到最后一张依次加载图片,形成“动画”. 生成BitmapImage的方法有多种: 1. var source=new BitmapImage(new Uri(" ...

  6. 解决android加载图片时内存溢出问题

    尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过jav ...

  7. 【Android】ListView、RecyclerView异步加载图片引起错位问题

    今天在RecyclerView列表里遇到一个情况,它包含300条数据,每项包含一个图片,发现在首次载入时,由于本地没图,请求网络的时候:快速滑动导致了图片错位.闪烁的问题. 原理的话有一篇已经说的很清 ...

  8. android 网络加载图片,对图片资源进行优化,并且实现内存双缓存 + 磁盘缓存

    经常会用到 网络文件 比如查看大图片数据 资源优化的问题,当然用开源的项目  Android-Universal-Image-Loader  或者 ignition 都是个很好的选择. 在这里把原来 ...

  9. OpenCV使用:加载图片时报错 0x00007FFC1084A839 处(位于 test1.exe 中)有未经处理的异常: Microsoft C++ 异常: cv::Exception,位于内存位置 0x00000026ABAFF1A8 处。

    加载图片代码为: #include<iostream> #include <opencv2/core/core.hpp> #include <opencv2/highgu ...

随机推荐

  1. WPF自定义Main函数

    第一步:在app.xaml.cs里加入以下代码: [STAThread] public static void Main(string[] args) { App app = new App(); a ...

  2. MySQL查询优化处理

    查询的生命周期的下一步是将一个sql转化成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互.这包括多个子阶段:解析sql,预处理,优化sql执行计划.这个过程中任何错误(例如语法错误)都可 ...

  3. nginx配置使其支持thinkphp的pathinfo模式

    #user root;#user nobody;worker_processes 1; #error_log logs/error.log;#error_log logs/error.log noti ...

  4. Python Socket Programming

    本文介绍使用Python进行Socket网络编程,假设读者已经具备了基本的网络编程知识和Python的基本语法知识,本文中的代码如果没有说明则都是运行在Python 3.4下. Python的sock ...

  5. 三种实现PHP伪静态页面的方法(转)

    PHP伪静态写法--其一 伪静态又名:URL重写 以下列举了三种方法. 方法一: 比如这个网页 http://www.2cto.com /soft.php/1,100,8630.html 其实处理的脚 ...

  6. Swift—final关键字-b

    在类的定义中使用final关键字声明类.属性.方法和下标.final声明的类不能被继承,final声明的属性.方法和下标不能被重写. 下面看一个示例: final class Person { //声 ...

  7. 安卓u8800刷机

    一篇非常好的帖子:http://bbs.anzhi.com/thread-5113728-1-1.html 虽然不是什么大神,不过在两个QQ群里和这里解答过N多刷机和ROOT中遇到的问题了...而且伸 ...

  8. 【HDOJ】1520 Anniversary party

    第二道树形DP,先是MLE.后来仅需改小邻接矩阵的第二个维度到30就过了. #include <cstdio> #include <cstring> #include < ...

  9. cf509B Painting Pebbles

    B. Painting Pebbles time limit per test 1 second memory limit per test 256 megabytes input standard ...

  10. 转载-常用API接口签名验证参考

    原文地址: http://www.cnblogs.com/hnsongbiao/p/5478645.html 写的很好,就做个笔记了.感谢作者! 项目中常用的API接口签名验证方法: 1. 给app分 ...