BitmapPool 了解吗?Glide 是如何实现 Bitmap 复用的?

这个问题实际上是我前几天面试时遇到的一个问题,虽然我之前分析过 Glide 的源码,但是老实说,如果不是面试遇到这类问题,我根本不会留意 Glide 的 Bitmap 复用这块…不管怎么说,遇到了这个问题,我们就来看下 Glide 是如何实现 Bitmap 复用的吧~

1、“池化”以及对象复用

其实,说起“池化”以及对象复用,在 Android 中例子还是有这么几个的。典型的比如 Handler 中的 Message. 当我们使用 Message 的 obtain 获取消息的时候,实际上是从 Message 池中获取的。Handler 中的 Message 是通过链表维护的数据结构,以此来构成一个 “Message 池”。这个池的最大数量由 MAX_POOL_SIZE 这个参数指定,即为 50.

那么,“池化”以及对象复用有什么好处呢?

这是因为对于 Message 这类频繁使用的对象,如果每次使用的时候直接创建一个对象,那么可能会因频繁创建和销毁导致虚拟机 GC,从而造成页面卡顿现象,尤其是在低端设备上面。“池化”之后每次从池子中获取已经创建的对象进行复用,从而避免了虚拟机频繁 GC.

对于 Bitmap 这类对象和图片相关、占用内存较大的对象,如果频繁创建和销毁,对虚拟机的影响可能比 Message 要大得多,因此 Bitmap 复用显得非常重要。

2、从 Bitmap 的回收说起

先看下 Bitmap 是如何进行回收的吧。

根据官方的建议,在 Android 2.3 及以下的版本中建议使用 recycle() 回收内存,防止 OOM. 但是,使用这个方法的前提是需要确保这个位图不再被使用,否则回收之后再使用将会导致运行时错误。所以,官方的建议是通过引用计数的方式统计位图的引用,只有当位图不再被引用的时候再真正调用该方法进行回收。

官方文档参考:https://developer.android.com/topic/performance/graphics/manage-memory

在 Android 3.0 上面引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在加载内容时尝试重复使用现有位图。这样可以复用现有的 Bitmap,减少对象创建,从而减少发生 GC 的概率。不过,inBitmap 的使用方式存在某些限制。特别是在 Android 4.4(API 级别 19)之前,系统仅支持大小相同的位图。在 Android 4.4 之后的版本,只要内存大小不小于需求的 Bitmap 都可以复用。

所以,当我们需要在 Android 中使用 Bitmap 的时候,应该考虑进行 Bitmap 复用以提升应用性能。但是,这些复杂的逻辑要如何封装呢?官方的建议是使用比较成熟的图片加载框架,比如 Glide. 所以,接下来我们来分析下 Glide 是如何实现 Bitmap 复用的。

3、Glide 的 BitmapPool

我们直接从 Glide 的 BitmapPool 开始分析。BitmapPool 是一个接口,定义如下:

public interface BitmapPool {
  long getMaxSize();
  void setSizeMultiplier(float sizeMultiplier);
  // 往 pool 中插入 bitmap 以备复用
  void put(Bitmap bitmap);
  // 从 pool 中获取 bitmap 以复用
  @NonNull Bitmap get(int width, int height, Bitmap.Config config);
  @NonNull Bitmap getDirty(int width, int height, Bitmap.Config config);
  void clearMemory();
  void trimMemory(int level);
}

BitmapPool 通过定义一个 Pool 来让用户复用 Bitmap 对象。在 Glide 中,BitmapPool 有一个默认的实现 LruBitmapPool. 顾名思义,也是基于 LRU 的理念设计的。

前面我们提到过 inBitmap 以 Android 4.4 为分水岭,之前和之后的版本在使用上存在版本差异,那么 BitmapPool 是如何处理这个差异的呢?答案是策略模式。Glide 定义了 LruPoolStrategy 接口,该接口内部定义了增删相关操作。真实的 Bitmap 数据根据尺寸和颜色等映射关系存储到 LruPoolStrategy 中。BitmapPool 的 get 和 put 也是通过 LruPoolStrategy 的 get 和 put 完成的。

interface LruPoolStrategy {
  void put(Bitmap bitmap);
  @Nullable Bitmap get(int width, int height, Bitmap.Config config);
  @Nullable Bitmap removeLast();
  String logBitmap(Bitmap bitmap);
  String logBitmap(int width, int height, Bitmap.Config config);
  int getSize(Bitmap bitmap);
}

LruPoolStrategy 默认提供了三个实现,分别是 AttributeStrategy、SizeConfigStrategy 和 SizeStrategy. 其中,AttributeStrategy 适用于 Android 4.4 以下的版本,SizeConfigStrategy 和 SizeStrategy 适用于 Android 4.4 及以上的版本。

AttributeStrategy 通过 Bitmap 的 width(图片宽度)、height(图片高度) 和 config(图片颜色空间,比如 ARGB_8888 等) 三个参数作为 Bitmap 的唯一标识。当获取 Bitmap 的时候只有这三个条件完全匹配才行。而 SizeConfigStrategy 使用 size(图片的像素总数) 和 config 作为唯一标识。当获取的时候会先找出 cofig 匹配的 Bitmap(一般就是 config 相同),然后保证该 Bitmap 的 size 大于我们期望的 size 并且小于期望 size 的 8 倍即可复用(可能是为了节省内存空间)。

所谓的 LRU 就是 BitmapPool 通过 LruPoolStrategy 实现的,具体操作是,在往 BitmapPool 中 put 数据之后会执行下面的操作调整空间大小:

private synchronized void trimToSize(long size) {
    while (currentSize > size) {
        // 移除尾部的
        final Bitmap removed = strategy.removeLast();
        if (removed == null) {
            currentSize = 0;
            return;
        }
        currentSize -= strategy.getSize(removed);
        // ...
        // 回收
        removed.recycle();
    }
}

4、Bitmap 加载和复用

下面我们来复习下一般的 Bitmap 加载的步骤。常规的图片加载过程如下,

// 设置 inJustDecodeBounds 为 true 来获取图片尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// 设置 inJustDecodeBounds 为 false 来真正加载
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);

也就是说,首先通过设置 options.inJustDecodeBounds 为 true 来获取图片真实的尺寸,以便设置采样率。因为我们一般不会直接加载图片的所有的像素,而是采样之后再按需加载,以减少图片的内存占用。当真正需要加载的时候,设置 options.inJustDecodeBounds 为 false,再调用 decode 相关的方法即可。

那么 Bitmap 复用是如何使用的呢?很简单,只需要在加载的时候通过 options 的 inBitmap 参数指定一个 Bitmap 对象再 decode 即可:

options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);

5、Glide 是如何加载 Bitmap 的

之前分析 Glide 的源码的时候,注重的是整个流程,对于很多细节没用照顾到,这里我简化下逻辑。首先,Glide 的 Bitmap 加载流程位于 Downsampler 类中。当从其他渠道,比如网络或者磁盘中获取到一个输入流 InputStream 之后就可以进行图片加载了。下面是 Downsampler 的 decodeFromWrappedStreams 方法,这里是执行图片加载的流程,主要代码的逻辑和功能已经备注到了注释上面:

  private Bitmap decodeFromWrappedStreams(InputStream is,
      BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
      DecodeFormat decodeFormat, ...) throws IOException {
    long startTime = LogTime.getLogTime();
    // 通过设置 inJustDecodeBounds 读取图片的原始尺寸信息
    int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
    int sourceWidth = sourceDimensions[0];
    int sourceHeight = sourceDimensions[1];
    String sourceMimeType = options.outMimeType;
    // ...
    // 读取图片的 exif 信息,如果需要的话,先对图片进行旋转
    int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);
    int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
    boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);
    int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
    int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
    ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
    // 根据要求计算需要记载的图片大小和 config,计算结果直接设置给 options 即可
    calculateScaling(imageType, is, ..., options);
    calculateConfig(is, ..., options, targetWidth, targetHeight);
    boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
      // ...
      // 根据图片的期望尺寸到 BitmapPool 中获取一个 Bitmap 以复用
      if (expectedWidth > 0 && expectedHeight > 0) {
        setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
      }
    }
    // 开始执行 decode 逻辑
    Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
    callbacks.onDecodeComplete(bitmapPool, downsampled);
    // ... 图片旋转等后续逻辑
    return rotated;
  }

上述代码中的 setInBitmap 方法中即调用了 BitmapPool 的 get 方法用来获取复用的 Bitmap 对象,其代码如下:

  private static void setInBitmap(
      BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
    @Nullable Bitmap.Config expectedConfig = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      if (options.inPreferredConfig == Config.HARDWARE) {
        return;
      }
      expectedConfig = options.outConfig;
    }
    if (expectedConfig == null) {
      expectedConfig = options.inPreferredConfig;
    }
    // 调用了 inBitmap
    options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
  }

// LruBitmapPool
  public Bitmap getDirty(int width, int height, Bitmap.Config config) {
    // 优先获取
    Bitmap result = getDirtyOrNull(width, height, config);
    if (result == null) { // 若没有,新建一个bitmap
      result = createBitmap(width, height, config);
    }
    return result;
  }

另外,通过查看 Bitmap 的 inBitmap 文档注释,我们可以看到可能存在一些情况导致 inBitmap 过程中出现异常,那么 Glide 会不会因为复用 Bitmap 而导致加载过程异常?Glide 又是如何进行处理的呢?参考上述代码,我们可以看到加载图片调用到了名为 decodeStream 方法。该方法经过我的简化之后大致如下:

  private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options,
      DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
    // ...
    final Bitmap result;
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      // 数据加载
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      // ...
      if (options.inBitmap != null) {
        try {
          // 输入流重置
          is.reset();
          bitmapPool.put(options.inBitmap);
          // 清理掉 inBitmap 并进行第二次加载
          options.inBitmap = null;
          // 再次调用进行加载
          return decodeStream(is, options, callbacks, bitmapPool);
        } catch (IOException resetException) {
          throw bitmapAssertionException;
        }
      }
      throw bitmapAssertionException;
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }
    if (options.inJustDecodeBounds) {
      is.reset();
    }
    return result;
  }

也就是说,Glide 首先会通过设置 inBitmap 复用的方式加载图片。如果这个过程中出现了异常,因为此时 inBitmap 不为空,所以将会进入异常处理流程,此时会清理掉 inBitmap,再次调用 decodeStream 方法二次加载,这个时候就不是 Bitmap 复用的了。所以,Glide 内部会通过错误重试机制进行 Bitmap 复用,当复用并出现错误的时候,会降级为非复用的方式第二次进行加载。

注意:高版本的glide代码跟之前的相比有比较大的改动。

Using Bitmap Pools in Android这篇文章中,获取inBitmap,实现内存复用相关的代码(glide低版本,比较好理解)如下:

// 获取inBitmap,实现内存复用
public Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
     Bitmap bitmap = null;

     if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
         final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
         Bitmap item;

         while (iterator.hasNext()) {
             item = iterator.next().get(
             if (null != item && item.isMutable()) {
                if (canUseForInBitmap(item, options)) {
                     Log.v("TEST", "canUseForInBitmap!!!!");

                     bitmap = item;

                     // Remove from reusable set so it can't be used again
                     iterator.remove();
                     break;
                 }
             } else {
                 // Remove from the set if the reference has been cleared.
                 iterator.remove();
              }
         }
    }

    return bitmap;
}

上述方法从软引用集合中查找规格可利用的Bitamp作为内存复用对象,因为使用inBitmap有一些限制,在Android 4.4之前,只能复用相同大小Bitmap的内存区域,在Android4.4以后的版本,可以重用任何 Bitmap 的内存区域,只要这块内存比将要分配内存的 bitmap 大就可以(参考:Android Develop)。因此使用了canUseForInBitmap方法来判断该Bitmap是否可以复用,代码如下所示:

@TargetApi(Build.VERSION_CODES.KITKAT)
private static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        return candidate.getWidth() == targetOptions.outWidth
                && candidate.getHeight() == targetOptions.outHeight
                && targetOptions.inSampleSize == 1;
    }
    int width = targetOptions.outWidth / targetOptions.inSampleSize;
    int height = targetOptions.outHeight / targetOptions.inSampleSize;

    int byteCount = width * height * getBytesPerPixel(candidate.getConfig());

    return byteCount <= candidate.getAllocationByteCount();
}

总结

以上就是 Glide 中 Bitmap 复用的原理,希望本文对你有所帮助!


探究Bitmap在Android中的内存占用

一、Bitmap的内存占用检测

Bitmap 一直以来都是 Android App 的内存消耗大户,很多 Java 甚至 native 内存问题的背后都是不当持有了大量大小很大的 Bitmap,我们可以使用Android Studio自带的Profile进行检测,由于Bitmap不会持有Context,所以,Profile无法检测出Bitmap导致的内存泄漏问题,但是重复创建Bitmap而没有及时回收,则会导致大量的内存占用,需要我们注意。对于Bitmap的内存监控,具体步骤如下:

1、进行内存Dump

在怀疑应用内存占用不正常的时间点内,点击Profile左侧的Capture heap dump,Android Studio会记录当前状态下应用中正在存活在内存中的实例对象

2、过滤Bitmap

在搜索框中过滤Bitmap,结果中会显示当前进程中所有存活的Bitmap对象,我们可以按照大小进行排序,然后一次点击其中的某一个Bitmap,从右侧的References中定位到此Bitmap的创建位置,需要注意的是,由我们调用系统API创建的Bitmap也会显示在这里,这里我们可以优先排查检测我们自己创建的Bitmap

  • Shallow size就是对象本身占用内存的大小,不包含其引用的对象;

  • Retained size是该对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。换句话说,retained size是该对象被GC之后所能回收到内存的总和

二、Bitmap的内存占用分析

通过上面的分析定位,可以找到内存占用较大的Bitmap,我们通常采用的方案,是对图片进行压缩,减少其内存占用情况,常用的压缩方案有以下几种,我们逐一来进行分析

我们这里采用的是一张分辨率为 4400 * 2475 的图片

1、默认大小

放到Assets文件夹下,读取后加载到ImageView中:

打印得到的内存大小为:41.54MB

private fun loadAssets() {
    val bytes = assets.open("big_picture.jpg").readBytes()
    val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
    list.add(bitmap)
    Log.d(TAG, "原始图片大小:${bitmap.allocationByteCount / 1024.0 / 1024.0}")
}
2、Drawable大小

把文件放到drawable文件夹下,通过R.drawable.xxx的形式来读取:

打印得到的内存大小为:280.82MB

private fun loadDefault() {
    val decodeResource = BitmapFactory.decodeResource(resources, R.drawable.big_picture_default)
    val byteCount = decodeResource.allocationByteCount / 1024.0 / 1024.0
     Log.d(TAG, "Drawable大小:$byteCount")
}
3、Dwawable-XX大小

把文件放在drawable-xxhidp文件夹下,通过R.drawable.xxx的形式来读取:

打印得到的内存大小为:31.19MB

private fun loadXX() {
    val decodeResource = BitmapFactory.decodeResource(resources, R.drawable.big_picture_xx)
    val byteCount = decodeResource.allocationByteCount / 1024.0 / 1024.0
    Log.d(TAG, "DrawableXX大小:$byteCount")
}
4、采样率压缩

图片宽高分别压缩为原来的一半:

打印得到的内存大小为:10.38MB

/**
 * 尺寸压缩
 */
private fun sizeCompress() {
    val bytes = assets.open("big_picture.jpg").readBytes()
    val options = BitmapFactory.Options()
    options.inSampleSize = 2
    val outBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
    Log.d(TAG, "尺寸压缩1/2:${outBitmap.allocationByteCount / 1024.0 / 1024.0}")
}
5、质量压缩为原来的一半:

打印得到的内存大小为:41.54MB

/**
 * 质量压缩
 * 它其实只能实现对file的影响,对加载这个图片出来的bitmap内存是无法节省的,还是那么大。
 * 因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素
 */
private fun qualityCompress() {
    val bytes = assets.open("big_picture.jpg").readBytes()
    val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
    val stream = ByteArrayOutputStream()
    bitmap.compress(Bitmap.CompressFormat.JPEG, 50, stream)
    val byteArray = stream.toByteArray()
    val outBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
    Log.d(TAG, "质量压缩50%:${outBitmap.allocationByteCount / 1024.0 / 1024.0}")
}
6、像素压缩

RGB通道设置为RGB_565后:

打印得到的内存大小为:20.77MB

/**
 * RGB通道压缩
 */
private fun rgbCompress() {
    val bytes = assets.open("big_picture.jpg").readBytes()
    val options = BitmapFactory.Options()
    options.inPreferredConfig = Bitmap.Config.RGB_565
    val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
    Log.d(TAG, "RGB通道压缩RGB_565:${bitmap.allocationByteCount / 1024.0 / 1024.0}")
}
7、使用Glide设置图片:

打印得到的内存大小为:1.25MB

private fun glideCompress(){
    val s = object :RequestListener<Drawable>{
        override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
            return false
        }
 
        override fun onResourceReady(resource: Drawable, model: Any?, target: Target<Drawable>, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
            val width = resource.intrinsicWidth
            val height = resource.intrinsicHeight
            resource.setBounds(0,0,width,height)
            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
            Log.d(TAG, "Glide加载:${bitmap.allocationByteCount / 1024.0 / 1024.0}")
            return false
        }
    }
 
    val bytes = assets.open("big_picture.jpg").readBytes()
    Glide.with(this).load(bytes).listener(s).into(img_view)
 
}

三、分析与总结:

1、Bitmap的计算方式:

Bitmap文件在Android设备中的计算方式为:

Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小

2、BitmapFactory.Options中几个关于分辨率的属性参数:
  • inDensity:Bitmap位图自身的密度、分辨率

  • inTargetDensity: Bitmap最终绘制的目标位置的分辨率

  • inScreenDensity: 设备屏幕分辨率

其中inDensity和图片存放的资源文件的目录有关,同一张图片放置在不同目录下会有不同的值:

3、不同分辨率文件夹的缩放:

基于以上分析,可以得到以下几个结论:

同一张图片,放在不同资源目录下,其分辨率会有变化,

Bitmap存放文件夹分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少

图片不特别放置任何资源目录时,其默认使用mdpi分辨率:160

资源目录分辨率和设备分辨率一致时,图片尺寸不会缩放

4、结论验证

根据第三点的结论,结合上面的实验代码进行验证处理,文件放到drawable文件夹下,系统会默认当做mdpi来进行处理,而放到xx-hdpi下,则系统会当做二倍图来处理,根据这个计算规则,UI通常给的都是二倍图,我们把图片放到drawable-xxhdpi文件夹中,则会降低图片在系统中的内存占用,而如果把二倍图片放到drawable-xxxhdpi下,则会由于缩放原因图片变得模糊不清

5、质量压缩分析

质量压缩后图片在系统中的内存大小没有变化,这是因为质量压缩只能实现对文件存储的影响,对加载这个图片出来的bitmap内存是无法节省的,还是那么大。因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素

6、Glide加载分析

使用Glide加载图片占用的内存最小,这是因为Glide在加载图片时,会根据ImageView来实际计算控件真实需要的图片大小,并对原始图片做大小缩放,保证清晰度的情况下尽可能的降低图片占用的内存大小

四、Bitmap的内存优化方案

1、采用第三方加载库:

当需要匹配多种分辨率设备时,尽量采用Gilde等第三方图片加载框架进行图片加载,而不是直接使用img_view.setImageResource(R.drawable.big_picture_xx)的形式

2、图片检测分析库:

在开发阶段可以使用一些第三方的图片监控库,用来检测我们是否使用了超过实际使用宽高的图片,比如:BitmapCanary,在引用后,可以直接在图片控件中显示控件中加载的图片的宽高比例,如下图所示,上面的文字大小分别表明当前控件加载的图片宽高超过控件宽高的倍数

3、Bitmap的复用

采用Bitmap的缓存池,避免重复创建,由于Bitmap的创建不依赖于Context,因此Bitmap不会持有Context的引用,也就不会导致页面内存泄漏的问题出现,我们可以把需要经常用到的Bitmap添加到缓存池中,避免重复创建Bitmap带来的内存消耗,

注意:在Glide源码中,复用 Bitmap 之前,需要手动判断Bitmap是否可以被复用。这是因为 Bitmap 的复用有一定的限制,在Android 4.4版本之前,只能复用相同大小Bitmap的内存区域,在Android4.4以后的版本,可以重用任何 Bitmap 的内存区域,只要这块内存比将要分配内存的 bitmap 大就可以(参考:Android Develop),示例代码如下:

/**
 * candidate:旧的图片,targetOptions:新的图片的Options
 */
private fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
        val width: Int = targetOptions.outWidth / targetOptions.inSampleSize
        val height: Int = targetOptions.outHeight / targetOptions.inSampleSize
        val byteCount: Int = width * height * getBytesPerPixel(candidate.config)
        byteCount <= candidate.allocationByteCount
    } else {
        // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
        candidate.width == targetOptions.outWidth
                && candidate.height == targetOptions.outHeight
                && targetOptions.inSampleSize == 1
    }
}

如果不使用glide框架,怎么实现Bitmap的复用呢?

Android 3.0(Api 11)开始,Bitmap引入了BitmapFactory.Options.inBitmap字段,如果设置了inBitmap,那么采用Options对象的解码方法会在加载内容的同时尝试复用现有的Bitmap.

Bitmap的复用是有限制的,Android 4.4(Api 19)前后也有很大的差别,看官方说明:

  • Android 4.4开始

  • Android 4.4之前

先看怎么用。

  • 不复用

  • 复用

结果分析:

分析之前先介绍下BitmapFactory.Options.inMutablegetByteCount()getAllocationByteCount(),还是看官方介绍:

  • BitmapFactory.Options.inMutable

  • getByteCount()

  • getAllocationByteCount()

结果分析:

  • 不复用的情况,每创建一个bitmap都会分配一个对应大小的内存,且getByteCount()=getAllocationByteCount()

  • 复用的情况,bitmap2的getAllocationByteCount()是大于getByteCount()的,说明它是复用的bitmap1的。

单看打印的结果说明不了内存使用的情况,可以使用Android Studio的Profiler功能自行查看。

问题来了:如果使用了bitmap的复用,且解码的bitmap大小比被复用的bitmap还要大,会怎么样?
答:会crash,抛throw new IllegalArgumentException("Problem decoding into existing bitmap")异常,因为不够。
4. 合理管理图片的内存缓存

为了提高图片的加载速度,我们通常会对图片做适当的缓存,但是缓存占用的内存过大时,会导致系统频繁GC从而引发卡顿,这里,系统提供了API告诉我们可以在合理的时机释放内存,当系统回调onLowMemory()时,我们可以尝试释放缓存中的图片资源,用来释放内存

class MainApplication : Application() {
 
    override fun onLowMemory() {
        super.onLowMemory()
    }
}
5、采用设备分级策略:

比如某些设备的RAM为2G,某些设备的RAM为4G,

当我们检测到设备的内存为2GB时,一般为低端机型,我们可以采用一些手段降低应用的内存占用,比如设置图片的色彩通道为RGB_565,图片内存上限为10MB等,

当我们检测到设备的内存为4G甚至6G以上时,这类设备通常为高端机型,为了用户体验,提高图片的加载速度,我们可以在此类设备中设置色彩通道为RGB_888,内存上限设置为20MB或者更大

这里为判断设备RAM的代码:

public static String getRAMInfo(Context context) {
   ActivityManager activityManager = (ActivityManager) context
                .getSystemService(context.ACTIVITY_SERVICE);
   ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
   activityManager.getMemoryInfo(memoryInfo);
   long totalSize = memoryInfo.totalMem;
  return Formatter.formatFileSize(context, totalSize);
}
6、合理选择像素解析:

Bitmap.Config参数:

  • ALPHA_8:只有透明度,没有任何RGB值,一个像素占用1个字节

  • ARGB_4444:每一个像素点由4个,A(Alpha)、R(Red)、G(Green)、B(Blue)值表示。每一个占用4位(bit),所以总共占用16bit=16/8(byte)=2byte。也即每一个像素占用2字节

  • ARGB_8888:Android手机上默认的格式,同时也是电脑上通用的格式。也是由4个值表示的,但每一个值占用8bit,所以总共是32bit=4byte。也即每一个像素占用4个字节

  • RGB_565:没有透明度(alpha),所以,没法支持半透明或是全透明。只有R(占用5bit),G(占用6bit),B(占用5bit),总共16bit=2byte。也即,每一个像素占用2字节

Android默认是按照ARGB_8888来解析的,当我们采用RGB_565解析时,每个像素的占用字节从4个降低为两个,可以降低内存占用,但是需要注意的是RGB_565不包含透明度,因此如果你的图片包含透明度时,优先采用ARGB_8888来解析,如果设备比较低端而内存吃紧的话,也可以采用ARGB_4444来解析,但是显示效果就会受到影响

7、分片加载图片:

有时候我们想要加载显示的图片很大或者很长,比如手机滚动截图功能生成的图片。针对这种情况,在不压缩图片的前提下,不建议一次性将整张图加载到内存,而是采用分片加载的方式来显示图片部分内容,然后根据手势操作,放大缩小或者移动图片显示区域。然后根据用户的滑动方向来继续渲染剩余的图片部分

Android SDK 中的 BitmapRegionDecoder 来实现分片加载的策略,示例代码如下:

/**
* 只渲染前200*200像素的内容
*/
private fun showRegionImage(){
    val stream = assets.open("big_picture.jpg")
    val decoder = BitmapRegionDecoder.newInstance(stream, false)
    val options = BitmapFactory.Options()
    val bitmap = decoder.decodeRegion(Rect(0, 0, 200, 200), options)
    img_view.setImageBitmap(bitmap)
}
8. 合理存放资源文件的位置

当UI图片的实际分辨率与展示分辨率相同时,图片不会进行缩放处理,此时的图片大小近似的相当于Android设备实际渲染大小

9. 及时释放Bitmap内存

下表是不同SDK版本中Bitmap对象和像素存放位置的区别(来源于Android Develop),

不同Android版本中的Java堆内存个Native内存存放地址也不一样,其回收方式也不一样,下表中为不同存放位置中释放内存的时机(来源于:Android中Bitmap内存优化

五、线上Bitmap内存监控上报

这里根据微信Matrix的内存监控策略,我们可以设计一个简单的内存统计工具,封装全局图片加载框架,通过WeakHashMap在图片加载前,记录下每个Bitmap的内存大小和唯一id,唯一id用来追溯Bitmap的创建来源,当系统调用onLowMemory()或者OOM时(OOM时,因为主进程已经崩溃,因此需要在新的进程中上报结果),把内存统计结果上报到服务器,开发人员就可以根据这个上报结果定位占用内存较大的图片资源,从而分析解决,示例代码如下:

class BitmapMemoryMonitor {
 
    /**
     * 用来记录所有的Bitmap内存占用情况
     * 弱引用的形式,避免引用Bitmap导致无法释放
     */
    private val bitmapHashMap = WeakHashMap<Bitmap, String>()
 
    fun recordBitmap(bitmap: Bitmap, id: String) {
        bitmapHashMap[bitmap] = id
    }
 
    /**
     * 上报当前bitmap的内存占用情况
     */
    fun reportBitmapMemory() {
        val map = mutableMapOf<String, Int>()
        bitmapHashMap.filter { it.key != null }
            .forEach {
                val bitmap = it.key
                val id = it.value
                map[id] = bitmap.allocationByteCount
            }
    }
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值