Android Bitmap详解

目录

Bitmap.png

1、Bitmap到底占多少内存

1.1、Android API计算方式

  • 在API12开始提供了getByteCount()方法,用来计算Bitmap所占的内存。
  • 在API19开始getAllocationByteCount()方法代替了getByteCount()。

1.1.1、getByteCount()和getAllocationByteCount()区别

  • 一般情况下两者是相等的。
  • 在Bitmap复用的情况下,getByteCount()表示新解码图片所占内存大小(并非实际大小,实际大小是复用的那个Bitmap的大小),而getAllocationByteCount()则表示被复用Bitmap所占的内存大小。

1.2、Bitmap占用内存的计算公式

从磁盘加载或者从网络加载的计算公式如下:

图片的长度 * 图片的宽度 * 一个像素点占用的字节数

如果从资源文件夹中加载,会怎么样呢?
首先把同一张图片放进不同的资源文件夹会发生什么?

  • 同一张图片放进不同的文件夹,图片会被压缩

加载资源文件中图片调用BitmapFactory.decodeResource()方法,该方法内部最终调用如下底层代码进行图片的压缩。

if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
    const int density = env->GetIntField(options, gOptions_densityFieldID);
    const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
    const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
    if (density != 0 && targetDensity != 0 && density != screenDensity) {
        scale = (float) targetDensity / density;
    }
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();

if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
    const float sx = scaledWidth / float(decoded->width());
    const float sy = scaledHeight / float(decoded->height());
    bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
    bitmap->allocPixels(&javaAllocator, NULL);
    bitmap->eraseColor(0);
    SkPaint paint;
    paint.setFilterBitmap(true);
    SkCanvas canvas(*bitmap);
    canvas.scale(sx, sy);
    canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}

我们可以看到压缩比例由如下公式得出

scale = (float) targetDensity / density;

图片的缩放比例和targetDensity和density有关,targetDensity是设备的屏幕像素密度,density是图片对应的资源文件夹对应的屏幕像素密度。
其中density和Bitmap存放的资源目录有关,不同的资源目录有不同的值

density 0.75 1 1.5 2 3 4
densityDpi 120 160 240 320 480 560
DpiFolder ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi

可以得出如下结论:

  • 1、同一张图片放在不同的目录下,分辨率会发生变化。
  • 2、图片不在资源目录中(如drawable中),其使用的默认dpi为160。
  • 3、当设备的像素密度和资源文件夹的像素密度相同时,加载图片时不发生缩放。
    放在资源文件夹下的图片占用内存计算公式如下:
Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (当前设备密度dpi/图片所在文件夹对应的密度dpi)^2 × 每个像素的字节大小

2、Bitmap内存优化从下面五个方面进行优化

  • 编码
  • 采样
  • 复用
  • 缓存
  • 匿名共享区

2.1、编码

Android 中提供以下几种编码

  • 1、ALPHA_8:表示8位Alpha位图,即A=8,它只有透明度没有颜色,1个像素占1个字节。
  • 2、ARGB_4444:表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占2个字节。
  • 3、RGB_565:表示16位RGB位图,即R=5,G=6,B=5。它没有透明度,一个像素点占2个字节。
  • 4、ARGB_8888:表示32位ARGB位图,即A=8,R=8,G=8,B=8。一个像素点占4个字节。
    可以通过修改图片的编码格式来修改一个像素点所占的内存大小,来达到修改Bitmap所占内存的目的。
Bitmap originBitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.reuse);
iv_origin.setImageBitmap(originBitmap);
Log.e(TAG,"原图大小:"+originBitmap.getAllocationByteCount());
BitmapFactory.Options options=new BitmapFactory.Options();
//一般通过修改Bitmap的编码格式为RGB_565来达到压缩目的时,不建议修改为ARGB_4444,图片失真严重
options.inPreferredConfig=Bitmap.Config.RGB_565;
Bitmap compressBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.reuse, options);
Log.e(TAG,"压缩后大小:"+compressBitmap.getAllocationByteCount());
iv_reused.setImageBitmap(compressBitmap);

输出log如下:

E/MainActivity: 原图大小:4762800
E/MainActivity: 压缩后大小:2381400

可见压缩后的图片所占内存大小为原图的一半,通过修改图片的编码格式可以实现图片的压缩。

2.2、采样

修改BitmapFactory.Options.inSampleSize可以修改图片的宽高,来达到修改图片的占用内存。

  BitmapFactory.Options options = new BitmapFactory.Options();
//不获取图片,不加载到内存中,只返回图片属性
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(photoPath, options);
//图片的宽高
int outHeight = options.outHeight;
int outWidth = options.outWidth;
Log.d("mmm", "图片宽=" + outWidth + "图片高=" + outHeight);
//计算采样率
int i = utils.computeSampleSize(options, -1, 1000 * 1000);
//设置采样率,不能小于1 假如是2 则宽为之前的1/2,高为之前的1/2,一共缩小1/4 一次类推
options.inSampleSize = i;
Log.d("mmm", "采样率为=" + i);
//图片格式压缩
//options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
float bitmapsize = getBitmapsize(bitmap);
Log.d("mmm","压缩后:图片占内存大小" + bitmapsize + "MB / 宽度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());

看下打印信息

07-09 11:02:11.714 8010-8010/com.example.jh.rxhapp D/mmm: 原图:图片占内存大小=45.776367MB / 宽度=4000高度=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 图片宽=4000图片高=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 采样率为=4
07-09 11:02:11.944 8010-8010/com.example.jh.rxhapp D/mmm: 压缩后:图片占内存大小1.4296875MB / 宽度=1000高度=750

这种我们根据BitmapFactory 的采样率进行压缩 设置采样率,不能小于1 假如是2 则宽为之前的1/2,高为之前的1/2,一共缩小1/4 一次类推,我们看到log ,确实起到了压缩的目的。

2.3、复用

Bitmap的复用就需要用到BitmapFactory.Options.inBitmap属性
这个属性又什么作用?

不使用这个属性,你加载三张图片,系统会给你分配三份内存空间,用于分别储存这三张图片。
如果用了inBitmap这个属性,加载三张图片,这三张图片会指向同一块内存,而不用开辟三块内存空间。

inBitmap的限制

  • 3.0-4.3
    -- 复用的图片大小必须相同
    -- 编码必须相同
  • 4.4以上
    -- 复用的空间大于等于即可
    -- 编码不必相同
  • 不支持WebP
  • 图片复用,这个属性必须设置为true; options.inMutable = true;

复用的实例

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
    public void reuseBitmap(View view) {
        BitmapFactory.Options  options=new BitmapFactory.Options();
        options.inMutable=true;
        options.inDensity=320;
        options.inTargetDensity=320;
        Bitmap  origin= BitmapFactory.decodeResource(getResources(),R.mipmap.origin,options);
        iv_origin.setImageBitmap(origin);
        Log.e(TAG,origin.toString());
        Log.e(TAG,"origin:getByteCount:"+origin.getByteCount()+",origin:getAllocationByteCount:"+origin.getAllocationByteCount());
        
        options.inDensity=320;
        options.inTargetDensity=160;
        options.inMutable=true;
        options.inBitmap=origin;

        Bitmap reuseBitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.reuse,options);
        iv_reused.setImageBitmap(reuseBitmap);
        Log.e(TAG,reuseBitmap.toString());
        Log.e(TAG,"origin:getByteCount:"+origin.getByteCount()+",origin:getAllocationByteCount:"+origin.getAllocationByteCount());
        Log.e(TAG,"reuseBitmap:getByteCount:"+reuseBitmap.getByteCount()+",reuseBitmap:getAllocationByteCount:"+reuseBitmap.getAllocationByteCount());
    }

2.4、图片的缓存

使用LruCache进行内存缓存。LruCache底层使用LinkedHashMap存储数据,并在达到设置的最大内存前将最近最少使用的数据删除。使用LruCache可以避免内存的频繁创建和销毁带来的内存开销。
在实现项目中还会结合DiskLruCache磁盘缓存一起使用。

2.5、匿名共享内存(Ashmem)

匿名共享内存是为了进程间共享数据分配的一块内存,在Android5.0之前,Bitmap可以存储在匿名共享内存上,实现像素数据伪存储在Native内存上,一个典型的例子就是Fresco,Fresco为了提高5.0之前图片处理的性能,就很有效的利用了这个特性,但是在Android5.0后就限制了匿名共享内存的使用。

3、图片到底储存在哪里?

2.3之前 3.0~7.1 8.0之后
bitmap对象 Java Heap Java Heap Java Heap
像素数据 Native Heap Java Heap Native Heap
迁移原因 ---- 解决Native Bitmap内存泄漏 共享整个系统的内存减少OOM
  • 在2.3之前像素数据是存储在Native内存上的,但是生命周期不可控,需要手动调用Bitmap.recycle()进行回收。
  • 在3.0~7.1之间,Bitmap像素数据存储在Dalvik的Java Heap上,甚至在4.4之前,可以在匿名共享内存上分配,实现像素数据伪存储在Native内存上。
  • 在8.0之后,Bitmap像素数据又重新回到了Native内存上,并且不需要手动回收内存,它共享整个系统的内存,极大的减少了OOM。
    比如在8.0手机上如果一直创建Bitmap,如果手机内存有1G,那么你的应用加载1G也不会OOM。

4、图片的压缩

图片的压缩一般有两种:

  • 通过修改采样率进行压缩,上面已经讲过。
  • 质量压缩。
bitmap.compress(Bitmap.CompressFormat.JPEG, 20, 
new FileOutputStream("sdcard/result.jpg"));
  • 质量压缩不会改变图片所占内存的大小,它改变的是图片存储的大小。

质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。

  • 质量压缩对png格式这种图片没有作用,因为png是无损压缩。
  • 质量压缩会导致图片失真。
  • 如果想压缩图片大小,还想做到不失真可以使用jpeg方式对图片压缩。具体可查看Android性能优化篇之图片压缩优化

5、如何加载高清大图

如果有需求,要求我们既不能压缩图片,又不能发生oom怎么办,这种情况我们需要加载图片的一部分区域来显示,下面我们来了解一下BitmapRegionDecoder这个类,加载图片的一部分区域,他的用法很简单。

//支持传入图片的路径,流和图片修饰符等
   BitmapRegionDecoder mDecoder = BitmapRegionDecoder.newInstance(path, false);
//需要显示的区域就有由rect控制,options来控制图片的属性
    Bitmap bitmap = mDecoder.decodeRegion(mRect, options);

由于要显示一部分区域,所以要有手势的控制,方便上下的滑动,需要自定义控件,而自定义控件的思路也很简单。

  • 1 提供图片的入口
  • 2 重写onTouchEvent, 根据手势的移动更新显示区域的参数
  • 3 更新区域参数后,刷新控件重新绘制
public class BigImageView extends View {

    private BitmapRegionDecoder mDecoder;
    private int mImageWidth;
    private int mImageHeight;
    //图片绘制的区域
    private Rect mRect = new Rect();
    private static final BitmapFactory.Options options = new BitmapFactory.Options();

    static {
        options.inPreferredConfig = Bitmap.Config.RGB_565;
    }

    public BigImageView(Context context) {
        super(context);
        init();
    }

    public BigImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public BigImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

    }

    /**
     * 自定义view的入口,设置图片流
     *
     * @param path 图片路径
     */
    public void setFilePath(String path) {
        try {
            //初始化BitmapRegionDecoder
            mDecoder = BitmapRegionDecoder.newInstance(path, false);
            BitmapFactory.Options options = new BitmapFactory.Options();
            //便是只加载图片属性,不加载bitmap进入内存
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(path, options);
            //图片的宽高
            mImageWidth = options.outWidth;
            mImageHeight = options.outHeight;
            Log.d("mmm", "图片宽=" + mImageWidth + "图片高=" + mImageHeight);

            requestLayout();
            invalidate();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //获取本view的宽高
        int measuredHeight = getMeasuredHeight();
        int measuredWidth = getMeasuredWidth();


        //默认显示图片左上方
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = mRect.left + measuredWidth;
        mRect.bottom = mRect.top + measuredHeight;
    }

    //第一次按下的位置
    private float mDownX;
    private float mDownY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = event.getX();
                float moveY = event.getY();
                //移动的距离
                int xDistance = (int) (moveX - mDownX);
                int yDistance = (int) (moveY - mDownY);
           
                if (mImageWidth > getWidth()) {
                    mRect.offset(-xDistance, 0);
                    checkWidth();
                    //刷新页面
                    invalidate();
                }
                if (mImageHeight > getHeight()) {
                    mRect.offset(0, -yDistance);
                    checkHeight();
                    invalidate();
                }
                mDownX = event.getX();
                mDownY = event.getY();
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
        }
        return true;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    /**
     * 确保图不划出屏幕
     */
    private void checkWidth() {


        Rect rect = mRect;
        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        if (rect.right > imageWidth) {
            rect.right = imageWidth;
            rect.left = imageWidth - getWidth();
        }

        if (rect.left < 0) {
            rect.left = 0;
            rect.right = getWidth();
        }
    }

    /**
     * 确保图不划出屏幕
     */
    private void checkHeight() {

        Rect rect = mRect;
        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        if (rect.bottom > imageHeight) {
            rect.bottom = imageHeight;
            rect.top = imageHeight - getHeight();
        }

        if (rect.top < 0) {
            rect.top = 0;
            rect.bottom = getHeight();
        }
    }
}

你可能感兴趣的:(Android Bitmap详解)