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();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容