fresco 拾遗

96
spiritTalk
2017.11.17 10:49 字数 1485
一、内存管理的独到之处

在Fresco中,提供了PlatformDecoder这个接口用于处理bitmap的decode过程,下面包括了该接口的具体实现类:

类关系图

在 ImagePipelineFactory 类中是根据不同的平台去 build 不同的 decoder:

  /**
   * Provide the implementation of the PlatformDecoder for the current platform using the
   * provided PoolFactory
   *
   * @param poolFactory The PoolFactory
   * @return The PlatformDecoder implementation
   */
  public static PlatformDecoder buildPlatformDecoder(
      PoolFactory poolFactory,
      boolean directWebpDirectDecodingEnabled) {
    // 5.0及以上,返回 ArtDecoder
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
      return new ArtDecoder(
          poolFactory.getBitmapPool(),
          maxNumThreads,
          new Pools.SynchronizedPool<>(maxNumThreads));
    } else {
      // directWebpDirectDecodingEnabled 为 true 且 小于4.4
      if (directWebpDirectDecodingEnabled
          && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        return new GingerbreadPurgeableDecoder();
      } else {// 其余情况,都使用 KitKatPurgeableDecoder
        return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
      }
    }
  }
/**
 * Bitmap decoder for ART VM (Lollipop and up).
 */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@ThreadSafe
public class ArtDecoder implements PlatformDecoder
/**
 * Bitmap decoder (Gingerbread to Jelly Bean). API 9(2.3) —> API 16(4.1)
 * <p/>
 * <p>This copies incoming encoded bytes into a MemoryFile, and then decodes them using a file
 * descriptor, thus avoiding using any Java memory at all. This technique only works in JellyBean
 * and below.
 * decode 前会先把原始数据(encoded data)copy 到 MemoryFile 中去,借助 MemoryFile 把 encoded data 
 * 拷贝到 ashmem 中去,尽量避免在 Java Heap 上分配内存而造成频繁 GC 的问题
 */
public class GingerbreadPurgeableDecoder extends DalvikPurgeableDecoder
/**
 * Bitmap Decoder implementation for KitKat
 *
 * <p>The MemoryFile trick used in GingerbreadPurgeableDecoder does not work in KitKat. Here, we
 * instead use Java memory to store the encoded images, but make use of a pool to minimize
 * allocations. We cannot decode from a stream, as that does not support purgeable decodes.
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
@ThreadSafe
public class KitKatPurgeableDecoder extends DalvikPurgeableDecoder
  • ArtDecoder 中通过 BitmapOptions 的 inBitmap 和 inTempStorage 去优化内存使用(inBitmap 是由 BitmapPool 去分配内存,inTempStorage 是由SynchronizedPool 分配内存,都是用缓存池的方式分配和回收内存,做到对这些区域的内存可管理,减少各个不同地方自行分配内存),最终去调用 BitmapFactory.decodeStream() 方法。
  • DalvikPurgeableDecoder 中设置 inPurgeable 和 inInputShareable 为 true,从注释看,inPurgeable 能使 bitmap 的内存分配到 ashmem 上;对于通过 filedescriptor 去decode的方式,还要设置 inInputShareable 为true,只能够使内存分配到 ashmem 上。
  private static BitmapFactory.Options getBitmapFactoryOptions(
      int sampleSize,
      Bitmap.Config bitmapConfig) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inDither = true; // known to improve picture quality at low cost
    options.inPreferredConfig = bitmapConfig;
    // Decode the image into a 'purgeable' bitmap that lives on the ashmem heap
    options.inPurgeable = true;
    // Enable copy of of bitmap to enable purgeable decoding by filedescriptor
    options.inInputShareable = true;
    // Sample size should ONLY be different than 1 when downsampling is enabled in the pipeline
    options.inSampleSize = sampleSize;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
      options.inMutable = true;  // no known perf difference; allows postprocessing to work
    }
    return options;
  }

为了理解Facebook到底做了什么工作,在此之前我们需要了解在Android可以使用的堆内存之间的区别。

  1. Java Heap(Dalvik Heap),这部分的内存区域是由Dalvik虚拟机管理,通过Java中 new 关键字来申请一块新内存。这块区域的内存是由GC直接管理,能够自动回收内存。这块内存的大小会受到系统限制,当内存超过APP最大可用内存时会OOM。
  2. Native Heap,这部分内存区域是在C++中申请的,它不受限于APP的最大可用内存限制,而只是受限于设备的物理可用内存限制。它的缺点在于没有自动回收机制,只能通过C++语法来释放申请的内存。
  3. Ashmem(Android匿名共享内存),这部分内存类似于Native内存区,但是它是受Android系统底层管理的,当Android系统内存不足时,会回收Ashmem区域中状态是 unpin 的对象内存块,如果不希望对象被回收,可以通过 pin 来保护一个对象。

Ashmem不能被Java应用直接处理,但是也有一些例外,图片就是其中之一。当你创建一张没有经过压缩的Bitmap的时候,Android的API允许你指定是否是可清除的。

BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

上面的代码便是通过设置 inPurgeable 为 true 来创建一个 Purgeable Bitmap ,这样decode出来的bitmap是在 Ashmem 内存中,GC无法自动回收它。当该Bitmap在被使用时会被 pin 住,使用完之后就 unpin ,这样系统就可以在将来某一时间释放这部分内存。

如果一个 unpinned 的 bitmap 在之后又要被使用,系统会运行时又将它重新 decode,但是这个 decode 操作是发生在UI线程中的有可能会造成掉帧现象,因此该做法已经被 Google 废弃掉,转为鼓励使用 inBitmap 来告知
bitmap 解码器去尝试使用已经存在的内存区域,新解码的 bitmap 会尝试去使用之前那张 bitmap 在 heap 中所占据的 pixel data 内存区域,而不是去问内存重新申请一块区域来存放 bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。

这听起来很完美,但是我们来看 inPurgeable:

        /**
         * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
         * ignored.
         *
         * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this
         * is set to true, then the resulting bitmap will allocate its
         * pixels such that they can be purged if the system needs to reclaim
         * memory. In that instance, when the pixels need to be accessed again
         * (e.g. the bitmap is drawn, getPixels() is called), they will be
         * automatically re-decoded.
         *
         * <p>For the re-decode to happen, the bitmap must have access to the
         * encoded data, either by sharing a reference to the input
         * or by making a copy of it. This distinction is controlled by
         * inInputShareable. If this is true, then the bitmap may keep a shallow
         * reference to the input. If this is false, then the bitmap will
         * explicitly make a copy of the input data, and keep that. Even if
         * sharing is allowed, the implementation may still decide to make a
         * deep copy of the input data.</p>
         *
         * <p>While inPurgeable can help avoid big Dalvik heap allocations (from
         * API level 11 onward), it sacrifices performance predictability since any
         * image that the view system tries to draw may incur a decode delay which
         * can lead to dropped frames. Therefore, most apps should avoid using
         * inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
         * allocations use the {@link #inBitmap} flag instead.</p>
         *
         * <p class="note"><strong>Note:</strong> This flag is ignored when used
         * with {@link #decodeResource(Resources, int,
         * android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
         * android.graphics.BitmapFactory.Options)}.</p>
         */
        @Deprecated
        public boolean inPurgeable;
  1. 在 LOLLIPOP(API 21—Android 5.0)时被弃用,fresco 5.0使用 inBitmap。
  2. 在 KITKAT 及之前使用设置为 true,当系统需要回收内存时,bitmap 的 pixels 可以被清除,当 pixels 需要被重新访问的时候(例如 bitmap draw或者调用 getPixels() 的时候),它们又可以重新被 decode 出来。
  3. 需要重新 decode 的话,自然需要 encoded data,encoded data 可能来源于对原始那份 encoded data 的引用,或者是对原始数据的拷贝。具体是引用或者拷贝,就是根据 inInputShareable 来决定的,如果是 true 那就是引用,不然就是 deep copy,但是 inInputShareable 即使设置为 true,不同的实现也可能是直接进行 deep copy。
  4. inPurgeable 能够避免大的 Dalvik heap 内存分配(从 API 11—Android
    3.0 开始),然而会牺牲 UI 的流畅性,因为重新 decode 的过程在 UI 线程中进行,会导致掉帧问题,因此不建议使用 inPurgeable,推荐使用 inBitmap 特性。
  5. 然而 inBitmap 这个特性直到 Android 3.0 之后才被支持,在 Android 4.4 之前重用的 bitmap 大小必须是一致的且必须是 jpeg 或 png 格式,从SDK 19(Android 4.4)开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小,新申请的bitmap与旧的bitmap必须有相同的解码格式。

那么如何解决 drop frames 导致的卡顿问题?

在 DalvikPurgeableDecoder 中可以看到,每次 decode 之后调用了 pinBitmap 方法。

  /**
   * Creates a bitmap from encoded bytes.
   *
   * @param encodedImage the encoded image with reference to the encoded bytes
   * @param bitmapConfig the {@link android.graphics.Bitmap.Config}
   * used to create the decoded Bitmap
   * @return the bitmap
   * @throws TooManyBitmapsException if the pool is full
   * @throws java.lang.OutOfMemoryError if the Bitmap cannot be allocated
   */
  @Override
  public CloseableReference<Bitmap> decodeFromEncodedImage(
      final EncodedImage encodedImage,
      Bitmap.Config bitmapConfig) {
    BitmapFactory.Options options = getBitmapFactoryOptions(
        encodedImage.getSampleSize(),
        bitmapConfig);
    CloseableReference<PooledByteBuffer> bytesRef = encodedImage.getByteBufferRef();
    Preconditions.checkNotNull(bytesRef);
    try {
      Bitmap bitmap = decodeByteArrayAsPurgeable(bytesRef, options);
      return pinBitmap(bitmap);
    } finally {
      CloseableReference.closeSafely(bytesRef);
    }
  }

为了让 inPurgeable 的 bitmap 不被自动 unpinned ,可以通过使用 jni 函数 AndroidBitmap_lockPixels() 函数来强制 pin bitmap ,这样我们就可以在 bitmap 被使用时不会被系统自动 unpinned ,从而也就避免了 unpinned 的 bitmap 在重新被使用时又会被重新 decode 而引起的掉帧问题。

这做后,Fresco 需要自己去管理这块内存区域,保证当这个 Bitmap 不再使用时,Ashmem 的内存空间能被 unpin,Fresco 选择在 Bitmap 离开屏幕可视范围时候(onDetachWindow等时候),通过调用 bitmap.recycle() 方法去做 unpin。

参考:
Fresco介绍 - 一个新的android图片加载库
谈谈fresco的bitmap内存分配
Fresco内存机制(Ashmem匿名共享内存)

coding
Web note ad 1