内存优化之二——Bitmap优化


用简单通俗的话来记录自己对架构的理解

1.内存增长分析

(1).内存增长问题定位

采用adb shell dumpsys meminfo + AS Profiler工具结合
在这里插入图片描述
可以从App Summary中看到,现在内存中占用最多的是NativeHeap。然后是Graphics,最后是Code。Graphics是GL这块的内存,一般对于应用层没有更好的办法优化,Code的代码块映射,我们可以从结合mmap文件进行分析,确定是哪里存在更多的冗余代码,进行突破。
对于Native Heap,主要是Native分配的内存。
在这里插入图片描述
结合Profiler的对象的分配,Bitmap的NativeSize这么高,所以,认定Bitmap是攻克点。
当然,我是习惯用命令行了,实际上,可以直接使用Profiler就可以查看Native Heap的分布。

2.Bitmap详解

(1).Bitmap的大小计算

一张 864x582 的 PNG 图片,我把它放到 drawable-xhdpi 目录下,在红米 Note3 上加载,占用内存是多少(1920x1080像素 ,5.5英寸 )? 我们清晰的知道 图片大小 = 宽 * 高 * 单个像素点所占字节数,那么这么一算大小应该是 864x582x4 = 2011392 ,但最终调用代码 Bitmap.getByteCount() 发现是 3465504。 难道我们刚刚所讲的公式不对?其实这里的宽高是 Bitmap 的宽高并不是资源图片的宽高:

Bitmap 大小 = bitmap.getWidth() * bitmap.getHeight() * 单个像素点所占字节数 = 1134 * 764 * 4 = 3465504。
那么Bitmap的宽高是如何来的呢?
Bitmap对于drawable的创建,是从BitmapFactory.decodeXX()。
所以,我们直接看BitmapFactory:

   public static Bitmap decodeResource(Resources res, int id, Options opts) {
        validate(opts);
        Bitmap bm = null;
        InputStream is = null; 
        
        try {
            final TypedValue value = new TypedValue();
            is = res.openRawResource(id, value);

            bm = decodeResourceStream(res, value, is, null, opts);
        } catch (Exception e) {
            /*  do nothing.
                If the exception happened on open, bm will be null.
                If it happened on close, bm is still valid.
            */
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // Ignore
            }
        }

        if (bm == null && opts != null && opts.inBitmap != null) {
            throw new IllegalArgumentException("Problem decoding into existing bitmap");
        }

        return bm;
    }

    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }
       
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        // 获取当前手机设备的 dpi 
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

    // 省略部分跟踪代码 ......

    private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
            Rect padding, Options opts);

Bitmap的创建直接从Java层到了Native。大家在 http://androidxref.com/ 上看,因为不同版本之间有差异,我们先只看一个版本。
这里以 Android N 版本为例:

/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

//代码比较长,只择取最关键部分,省得大家疲惫

static jobject doDecode(JNIEnv *env, SkStreamRewindable *stream, jobject padding, jobject options) {
    // This function takes ownership of the input stream.  Since the SkAndroidCodec
    // will take ownership of the stream, we don't necessarily need to take ownership
    // here.  This is a precaution - if we were to return before creating the codec,
    // we need to make sure that we delete the stream.
    std::unique_ptr<SkStreamRewindable> streamDeleter(stream);

    // Set default values for the options parameters.
    int sampleSize = 1;
    // 是否只是获取图片的大小
    bool onlyDecodeSize = false;
    SkColorType prefColorType = kN32_SkColorType;
    bool isMutable = false;
    float scale = 1.0f;
    bool requireUnpremultiplied = false;
    jobject javaBitmap = NULL;

    // Update with options supplied by the client.
    // 解析 options 参数
    if (options != NULL) {
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        // Correct a non-positive sampleSize.  sampleSize defaults to zero within the
        // options object, which is strange.
        if (sampleSize <= 0) {
            sampleSize = 1;
        }

        if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
            onlyDecodeSize = true;
        }

        // initialize these, in case we fail later on
        env->SetIntField(options, gOptions_widthFieldID, -1);
        env->SetIntField(options, gOptions_heightFieldID, -1);
        env->SetObjectField(options, gOptions_mimeFieldID, 0);
        // 解析 ColorType ,复用参数等等
        jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
        prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
        isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
        requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
        // 计算缩放的比例
        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
            // 获取图片当前 xhdpi 的 density
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            // 获取当前设备的 dpi
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
                // scale = 当前设备的 dpi / xhdpi 的 density
                // scale = 420/320 = 1.3125    ------------///1111111
                scale = (float) targetDensity / density;
            }
        }
    }

    // Create the codec.
    NinePatchPeeker peeker;
    std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(streamDeleter.release(),
                                                                        280 & peeker));
    if (!codec.get()) {
        return nullObjectReturn("SkAndroidCodec::NewFromStream returned null");
    }

    // Do not allow ninepatch decodes to 565.  In the past, decodes to 565
    // would dither, and we do not want to pre-dither ninepatches, since we
    // know that they will be stretched.  We no longer dither 565 decodes,
    // but we continue to prevent ninepatches from decoding to 565, in order
    // to maintain the old behavior.
    if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
        prefColorType = kN32_SkColorType;
    }
    // 获取当前图片的大小
    // Determine the output size.
    SkISize size = codec->getSampledDimensions(sampleSize);

    int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false;
    // 处理 simpleSize 压缩,我们这里没穿,上面默认是 1 
    // Apply a fine scaling step if necessary.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
        willScale = true;
        scaledWidth = codec->getInfo().width() / sampleSize;
        scaledHeight = codec->getInfo().height() / sampleSize;
    }

    // Set the options and return if the client only wants the size.
    if (options != NULL) {
        jstring mimeType = encodedFormatToString(env, codec->getEncodedFormat());
        if (env->ExceptionCheck()) {
            return nullObjectReturn("OOM in encodedFormatToString()");
        }
        // 设置 options 对象中的 outWidth 和 outHeight
        env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
        env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
        env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
        // 如果只是获取大小直接 return null 这里是 nullptr 而不是 NULL
        if (onlyDecodeSize) {
            return nullptr;
        }
    }

    // Scale is necessary due to density differences.
    if (scale != 1.0f) {
        willScale = true;
        // 计算 scaledWidth 和 scaledHeight ------------///22222222
        // scaledWidth = 864 * 1.3125 + 0.5f = 1134 + 0.5f = 1134
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
        // scaledHeight = 582 * 1.3125 + 0.5f = 763.875 + 0.5f = 764
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
    ........
    ........
    ........
}

来,让我们一起梳理一下思路,大家注意看下我做的备注------------///1和2。这两处是计算Bitmap大小的关键。
1.利用当前手机dpi与图片所在的drawable/mipmap的dpi比值,从而确定缩放率
2.利用当前的drawable分辨率计算Bitmap的宽高:scaledWidth * scale + 0.5f
这样的话,大家就清晰了吧?如果要优化本地的图片加载的内存,让我们的设计师动起来。

(2).Bitmap内存申请

(a)Bitmap的内存组成
Bitmap 的内存申请不同版本间有些许差异,Bitmap对象的构建分为两部分:
一部分是Bitmap对象内存,另一个是像素的内存
在 3.0-7.0 的 bitmap 像素内存都是存放在 Java heap 中的,而 8.0 以后则是放在 Native heap 中的。
我使用的是android 10.0,所以是Native heap比较大。

(b)Bitmap的像素内存最終为啥还是选择了Native内存?
其实,更好的防止OOM。
什么这么说呢?
创建一个2G的Bitmap,在8.0以前,会立即OOM,而在8.0之后则不会。

(3).Bitmap的内存回收机制

总结:其实无论是 Android M 前还是之后,释放 Native 层的 Bitmap 对象的思想都是去监听 Java 层的 Bitmap 是否被释放,一旦当 Java 层的 Bitmap 对象被释放则立即去释放 Native 层的 Bitmap 。只不过 Android M 前是基于 Java 的 GC 机制,而 Android M 后是注册 native 的 Finalizer 方法。
android只在2.3之前,是需要手动释放。bitmap.recycle();
看下8.0的Finalizer方法

/frameworks/base/graphics/java/android/graphics/Bitmap.java
 Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatchInsetStruct ninePatchInsets) {
        if (nativeBitmap == 0) {
            throw new RuntimeException("internal error: native bitmap is 0");
        }

        mWidth = width;
        mHeight = height;
        mIsMutable = isMutable;
        mRequestPremultiplied = requestPremultiplied;
        mBuffer = buffer;

        mNinePatchChunk = ninePatchChunk;
        mNinePatchInsets = ninePatchInsets;
        if (density >= 0) {
            mDensity = density;
        }

        mNativePtr = nativeBitmap;
        // 这个对象对象来回收
        mFinalizer = new BitmapFinalizer(nativeBitmap);
        int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
        mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
    }

    private static class BitmapFinalizer {
        private long mNativeBitmap;

        // Native memory allocated for the duration of the Bitmap,
        // if pixel data allocated into native memory, instead of java byte[]
        private int mNativeAllocationByteCount;

        BitmapFinalizer(long nativeBitmap) {
            mNativeBitmap = nativeBitmap;
        }

        public void setNativeAllocationByteCount(int nativeByteCount) {
            if (mNativeAllocationByteCount != 0) {
                VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
            }
            mNativeAllocationByteCount = nativeByteCount;
            if (mNativeAllocationByteCount != 0) {
                VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
            }
        }

        @Override
        public void finalize() {
            try {
                super.finalize();
            } catch (Throwable t) {
                // Ignore
            } finally {
                // finalize 这里是 GC 回收该对象时会调用
                setNativeAllocationByteCount(0);
                nativeDestructor(mNativeBitmap);
                mNativeBitmap = 0;
            }
        }
    }

    private static native void nativeDestructor(long nativeBitmap);

(4).Bitmap的复用

被复用的 Bitmap 必须为 Mutable(通过 BitmapFactory.Options 设置)
4.4 之前,将要解码的图像(无论是资源还是流)必须是 jpeg 或 png 格式且和被复用的 Bitmap 大小一样,其中BitmapFactory.Options#inSampleSize 字段必须设置为 1,要求比较严苛
4.4 以后,将要解码的图像的内存需要小于等于要复用的 Bitmap 的内存

/ 不复用的写法,消耗内存 32 M
  logMemory();
  Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test2);
  Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test2);
  logMemory();
  // 复用的写法,消耗内存 16 M
  logMemory();
  BitmapFactory.Options options = new BitmapFactory.Options();
  options.inMutable = true;
  Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test2, options);
  options.inBitmap = bitmap1;
  Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test2, options);
  logMemory();

(5).Bitmap的类型分类

private static Config sConfigs[] = {
            null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE
        };
类型诠释
ALPHA_8每个像素存储为单个半透明(alpha)通道。 每个像素需要1个字节的内存,只保存透明度,不保存颜色。似乎很不常用,应该是只存储ALPHA通道的一种遮罩类型图。创建此类型图后,无法在上面画颜色。
ARGB_4444每个像素存储在2个字节上,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位,此字段在API级别13中已弃用。由于此类型的图片配置导致画质质量较差,建议使用此字段ARGB_8888。
ARGB_8888每个像素存储在4个字节上。即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位。 每个通道(RGB和alpha为半透明)以8位精度(256个可能值)存储。这种配置非常灵活,可提供最佳质量。应尽可能使用它。
HARDWARE特殊配置,位图仅存储在图形内存中。
ARGB_8888每个像素存储在4个字节上。即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位。 每个通道(RGB和alpha为半透明)以8位精度(256个可能值)存储。这种配置非常灵活,可提供最佳质量。应尽可能使用它。
HARDWARE特殊配置,位图仅存储在图形内存中。
RGBA_F16每个像素存储在8个字节上。每个通道(RGB和半透明的alpha)存储为半精度浮点值。此配置特别适用于宽色域和HDR内容。
HARDWARE特殊配置,位图仅存储在图形内存中。
RGB_565每个像素存储在2个字节上,只有RGB通道被编码:红色以5位精度存储(32个可能值),绿色存储6位精度(64个可能值),蓝色存储5位精确。 此配置可能会产生轻微的视觉瑕疵,具体取决于源的配置。例如,没有抖动,结果可能会显示绿色。为了获得更好的结果,应该应用抖动。当使用不需要高色彩保真度的不透明位图时,此配置可能很有用。

(6).通道与位深

Bitmap的像素使用不同的通道进行表示。正如我们所知,颜色是由红绿蓝组成,这是基础色,这是我们学的初中技术知识。那么所有的颜色都是由它来表示。那么各个通道对应的值对应于对应的值表示。
这么说是不是有点枯燥。举个例子:
ARGB_4444这种类型的Bitmap实际表示的就是使用四通道:Alpha(透明度)、Red(红色)、Green(绿色)、Blue(蓝色)来表示,而后面的_4444则表示使用4位分别表示以上四个通道。那么一个像素所占内存:(4*4)/8= 2 Bit。这也是我们实际上在对图片的透明度和精确度无要求时,尽量是用的RGB_565的一个原因。

位深实际上理解是表示一个通道说使用的内存,而质量压缩,实际上就是进行位深的优化,它可以减少存储的大小的,但是,无法减少内存大小。

(7).Bitmap两种压缩方式

(a)质量压缩
不改变内存大小,只改变占用空间大小。本质是改变位深。
实现方式:

   /**
  * 压缩图片
  * 
  * @param bitmap
  *          被压缩的图片
  * @param sizeLimit
  *          大小限制
  * @return
  *          压缩后的图片
  */
 private Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     int quality = 100;
     bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);

     // 循环判断压缩后图片是否超过限制大小
     while(baos.toByteArray().length / 1024 > sizeLimit) {
         // 清空baos
         baos.reset();
         bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
         quality -= 10;
     }

     Bitmap newBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);

     return newBitmap;
 }

(b)尺寸压缩
改变内存大小,同时改变占用空间大小。本质是减少像素。
实现方式:

(1)利用采样率压缩:
 public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight) {
    final BitmapFactory.Options options = new BitmapFactory.Options();
    // 先将inJustDecodeBounds设置为true不会申请内存去创建Bitmap,返回的是一个空的Bitmap,但是可以获取            
    //图片的一些属性,例如图片宽高,图片类型等等。           
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 计算inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 加载压缩版图片
    options.inJustDecodeBounds = false;
    // 根据具体情况选择具体的解码方法
    return BitmapFactory.decodeResource(res, resId, options);
  }   
  
  public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
      // 原图片的宽高
      final int height = options.outHeight;
      final int width = options.outWidth;
      int inSampleSize = 1;

      if (height > reqHeight || width > reqWidth) {
          final int halfHeight = height / 2;
          final int halfWidth = width / 2;

          // 计算inSampleSize值
          while ((halfHeight / inSampleSize) >= reqHeight
            && (halfWidth / inSampleSize) >= reqWidth) {
                  inSampleSize *= 2;
           }
      }

      return inSampleSize;
  }
(2)Matrix 缩放法:
	Matrix matrix = new Matrix();
	matrix.setScale(0.5f, 0.5f);
	bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),bit.getHeight(), matrix, true);

(3)根据宽高比,采用最小压缩值开源:
Compressor:https://github.com/zetbaitsu/Compressor
这个开源库就是在普通的压缩算法上做了优化改进,源码很容易看懂,推荐!

(4)根据压缩效果前后对比确定压缩比
目前成熟的开源库有Luban:https://github.com/Curzibn/Luban
这个开源库算法比较复杂,根据效果图前后对比逆向推算了微信朋友圈的压缩,最后效果和微信差不多。

3.Bitmap的优化策略

(1).多屏幕适配本地图片

相同的图片放在不同的drawable/mipmap文件夹下,会对应于不同的dpi,从而会影响Bitmap大小的计算。详情可以看第一部分Bitmap的宽高计算。

(2).等比例压缩图片分辨率

使用Luban算法进行图片分辨率压缩

(3).Bitmap创建时复用,减少Native的内存大小

可见本文中的Bitmap的复用

(4).灵活选择Bitmap的类型

对于不需要透明度的图片,选择RGB_565,降低每个Bitmap的单个像素分辨率大小。
对于图片质量要求没有这么高的,选择ARGB_4444.

(5).减少Bitmap的缓存

以RecyclerView为例,
(1)默认离屏的缓存数据为2个,改为1个

recyclerView.setItemViewCacheSize(int);

(2)每种Type的Item默认是5个,也可以减少。

(3)Glide不进行内存缓存设置

 .skipMemoryCache(true) // 不使用内存缓存  
  .diskCacheStrategy(DiskCacheStrategy.NONE) // 不使用磁盘缓存  
  .into(imageView);

(6).Java层及时Bitmap的回收

(1)Bitmap的recycle。这个是针对8.0之前进行内存回收
(2)Glide的Bitmap回收:此处的结合更多是和Adapter的onViewRecycler一起使用

Glide.with(context).clear(imageView);

4.站在巨人肩膀

1.源码地址查看
http://androidxref.com/
2.Bitmap原理深入解析
https://www.jianshu.com/p/e430b95010c7

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值