一、内存管理的独到之处
在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可以使用的堆内存之间的区别。
- Java Heap(Dalvik Heap),这部分的内存区域是由Dalvik虚拟机管理,通过Java中 new 关键字来申请一块新内存。这块区域的内存是由GC直接管理,能够自动回收内存。这块内存的大小会受到系统限制,当内存超过APP最大可用内存时会OOM。
- Native Heap,这部分内存区域是在C++中申请的,它不受限于APP的最大可用内存限制,而只是受限于设备的物理可用内存限制。它的缺点在于没有自动回收机制,只能通过C++语法来释放申请的内存。
- 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;
- 在 LOLLIPOP(API 21—Android 5.0)时被弃用,fresco 5.0使用 inBitmap。
- 在 KITKAT 及之前使用设置为 true,当系统需要回收内存时,bitmap 的 pixels 可以被清除,当 pixels 需要被重新访问的时候(例如 bitmap draw或者调用 getPixels() 的时候),它们又可以重新被 decode 出来。
- 需要重新 decode 的话,自然需要 encoded data,encoded data 可能来源于对原始那份 encoded data 的引用,或者是对原始数据的拷贝。具体是引用或者拷贝,就是根据 inInputShareable 来决定的,如果是 true 那就是引用,不然就是 deep copy,但是 inInputShareable 即使设置为 true,不同的实现也可能是直接进行 deep copy。
- inPurgeable 能够避免大的 Dalvik heap 内存分配(从 API 11—Android
3.0 开始),然而会牺牲 UI 的流畅性,因为重新 decode 的过程在 UI 线程中进行,会导致掉帧问题,因此不建议使用 inPurgeable,推荐使用 inBitmap 特性。 - 然而 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匿名共享内存)