Android-Bitmap优化

作者: 愿天堂没Android | 来源:发表于2022-02-16 21:44 被阅读0次

    概述

    在日常开发中我们经常遇到加载图片报出oom的错误,我们要解决这个问题,首先要明白oom代表out of memory 内存溢出,因为手机内存有限,分给每个应用的内存有限,所以要解决这个问题就是要解决图片占用内存问题 android 中图片是以bitmap的形式存在的,那么bitmap中所占的内存,直接影响到了是否oom,我们了解一下bitmap的占用内存的计算方法

    Bitmap到底占多大内存

    从本地加载或者从网络加载可以用下面的公式计算

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

    如果从资源文件夹加载,会怎么样?

    首先把同一张图片放进不同的资源文件夹会发生什么?

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

    看下源码

    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:设备屏幕像素密度 dpi
    • density:图片对应的文件夹的像素密度 dpi

    其中density和Bitmap存放的资源目录有关,不同的资源目录有不同的值

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

    可以得出以下结论

    • 同一张图片放在不同的资源目录下,其分辨率会有变化
    • Bitmap的分辨率越高,其解析后的宽高越小,甚至小于原有的图片(及缩放),从而内存也响应的减少
    • 图片不放置任何资源目录时,其使用默认分辨率mdpi:160
    • 资源目录分辨率和屏幕分辨率一致时,图片尺寸不会缩放

    所以Bitmap在资源目录中的计算方式为

    Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (当前设备密度dpi/图片所在文件夹对应的密度dpi)^2 × 每个像素的字节大小
    
    

    Bitmap内存优化从下面四个方面进行优化

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

    下面我们一个个的来讲这些优化

    编码

    Android 中提供一下几种编码

    其中,A代表透明度;R代表红色;G代表绿色;B代表蓝色。

    • ALPHA_8 表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度
    • ARGB_4444 表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节
    • ARGB_8888 表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节
    • RGB_565 表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节

    也即是说我们可以通过改变图片格式,来改变每个像素占用字节数,来改变占用的内存,看下面代码

     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);
            //图片格式压缩
            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());
    
    

    看下log

    07-09 11:10:46.042 15312-15312/com.example.jh.rxhapp D/mmm: 原图:图片占内存大小=45.776367MB / 宽度=4000高度=3000
    07-09 11:10:46.043 15312-15312/com.example.jh.rxhapp D/mmm: 图片宽=4000图片高=3000
    07-09 11:10:46.367 15312-15312/com.example.jh.rxhapp D/mmm: 压缩后:图片占内存大小22.887695MB / 宽度=4000高度=3000
    
    

    宽高没变,我们改变了图片的格式,从ARGB_8888 变成了RGB_565 ,像素占用字节数减少了一般,根据log 内存也减少了一半,这种方式可行

    注意:由于ARGB_4444的画质惨不忍睹,一般假如对图片没有透明度要求的话,可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。

    采样

    我们了解到了计算bitmap的占用内存的方法 ,是以bitmap的宽高和每个像素占用的字节数决定的,下面我们分别讲一下俩个 的概念

    1 bitmap的宽高

    顾名思义,图片的大小就是bitmap的宽高,按公式我们可以缩减bitmap的宽高来达到压缩图片占用内存的目的,看下面代码,以缩减宽高来达到压缩的目的

      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 ,确实起到了压缩的目的

    复用

    图片复用指的是inBitmap这个属性

    这个属性又什么作用?

    不使用这个属性,你加载三张图片,系统会给你分配三份内存空间,用于分别储存这三张图片

    如果用了inBitmap这个属性,加载三张图片,这三张图片会指向同一块内存,而不用开辟三块内存空间

    inBitmap的限制

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

    匿名共享内存(Ashmem)

    Android 系统为了进程间共享数据开辟的一块内存区域,由于这块区域不受应用的Head的大小限制,相当于可以绕开oom,FaceBook的Fresco首次应用到实际中

    限制:5.0以后就限制了匿名共享内存的使用

    图片到底储存在哪里?

    2.3- 3.0-4.4 5.0-7.1 8.0
    Bitmap对象 java Heap java Heap java Heap
    像素数据 Native Heap java Heap Native Heap
    迁移原因 - 解决Native Bitmap内存泄露 共享整个系统的内存减少OOM

    8.0Bitmap的像素数据存储在Native,为什么又改为Native存储呢?

    因为8.0共享了整个系统的内存,测试8.0手机如果一直创建Bitmap,如果手机内存有1G,那么你的应用加载1G也不会oom

    LRU管理Bitmap

    我们可以利用LRU开管理Bitmap,给他设置内存最大值,及时回收

    图片的压缩

    图片的压缩一般有俩种

    • 通过采样压缩,上边已经讲过了
    • 质量压缩
    bitmap.compress(Bitmap.CompressFormat.JPEG, 20, 
    new FileOutputStream("sdcard/result.jpg"));
    
    

    这个大家用该都用过,这个压缩是保持像素的前提下改变图片的位深及透明度,来达到压缩的目的,不过这种压缩不会改变图片在内存中的带下,而且这种压缩会导致图片的失真,但是有没有压缩到100k左右,还不失真的方法?


    如何加载高清图

    如果有需求,要求我们既不能压缩图片,又不能发生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);
                    Log.d("mmm", "mDownX=" + mDownX + "mDownY=" + mDownY);
                    Log.d("mmm", "movex=" + moveX + "movey=" + moveY);
                    Log.d("mmm", "xDistance=" + xDistance + "yDistance=" + yDistance);
                    Log.d("mmm", "mImageWidth=" + mImageWidth + "mImageHeight=" + mImageHeight);
                    Log.d("mmm", "getWidth=" + getWidth() + "getHeight=" + getHeight());
                    if (mImageWidth > getWidth()) {
                        mRect.offset(-xDistance, 0);
                        checkWidth();
                        //刷新页面
                        invalidate();
                        Log.d("mmm", "刷新宽度");
                    }
                    if (mImageHeight > getHeight()) {
                        mRect.offset(0, -yDistance);
                        checkHeight();
                        invalidate();
                        Log.d("mmm", "刷新高度");
                    }
                    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();
            }
        }
    }
    
    
    

    代码行有注释,应该很好理解了

    作者:renxhui
    转载于::https://juejin.cn/post/6844903919479422984

    相关文章

      网友评论

        本文标题:Android-Bitmap优化

        本文链接:https://www.haomeiwen.com/subject/ykzllrtx.html