bitmap的内存分配主要有两个含义:
- 应用程序实现时针对bitmap的内存缓存;
- 从原始数据(byte[])经过decode生成bitmap过程中的内存分配问题;
其中第一个含义,在Fresco中实现对应的就是
interface MemoryCache,这里略有提及,不会大费章节,主要关注decode过程中的内存分配问题。
WHAT?
从
http://frescolib.org/关于memory的介绍可以知道,在4.x及以下的系统上,Fresco的bitmap decode会把bitmap的pixel data(像素数据)放到一个“
特殊的内存区域’
”
,这个特殊的内存区域其实就是ashmem,至于ashmem是什么,详细的可以参考罗升阳的博客(
http://blog.csdn.net/luoshengyang/article/details/6651971
),这里不详述。
WHY?
那为什么在4.x及以下系统需要这样做?原因是如果Bitmap数量很多时会占用大量的内存(这里内存特指Java Heap),必然就会更加频繁的触发虚拟机进行
GC
,
GC
会导致stop the world,就会出现卡顿,而5.0之后采用了art模式,对GC进行了优化,情况比较乐观(为什么这么说?另说),而在4.x及以下的手机就比较糟糕了,所以,在4.x及以下系统上Fresco选择了ashmem作为pixel data的存储区域。(对于想深究
GC
的问题,可自行分析dalvik和art的相关代码)
另外,Java Heap的GC问题既然会导致freeze的问题,那么我们也可以采用native heap,这个不受GC的影响,而且内存很大(基本可以认为只受物理内存大小影响),需要的只是特别注意去回收内存。那为什么不用native heap,而去选择ashmem呢?这个问题后面会讲解。
HOW?
Bitmap在Fresco这个框架是在decode时,内存被分配到ashmem中呢?接下来从代码上,慢慢分析这个过程。
PlatformDecoder
在
Fresco
中,提供了PlatformDecoder这个接口用于处理bitmap的decode过程,下面包括了该接口的具体实现类;
从类名就可以推测,Fresco根据系统版本不同,使用不同的decoder,从下面的代码也大致证实了这个推测(但有所差别):
/** * * source : com/facebook/imagepipeline/core/ImagePipelineFactory.java * */
/**
* 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,
BitmapCounterTracker bitmapCounterTracker) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
return new ArtDecoder(
poolFactory.getBitmapPool(),
maxNumThreads,
new Pools.SynchronizedPool<>(maxNumThreads));
} else {
if (directWebpDirectDecodingEnabled
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return new GingerbreadPurgeableDecoder(bitmapCounterTracker);
} else {
return new KitKatPurgeableDecoder(
poolFactory.getFlexByteArrayPool(), bitmapCounterTracker);
}
}
}
1.1
分别是,5.0以上都使用ArtDecoder,4.4以下如果directWebpDirectDecodingEnabled为true,则使用GingerbreadPurgeableDecoder,不然其他都使
用KitKatPurgeableDecoder
。
这些decoder主要包含两个方法对原始数据进行decode:decodeFromEncodedImage和decodeJPEGFromEncodedImage。
由于在decode环节上,这两个方法并无二致,为了方便,所以就选择
decodeFromEncodedImage进行深入分析。
DalvikPurgeableDecoder
先看DalvikPurgeableDecoder的decodeFromEncodedImage方法:
/** * * source : com/facebook/imagepipeline/platform/DalvikPurgeableDecoder.java * */
@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);
}
}
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;
}
2.1
着重关注和内存分配相关的两个设置:inPurgeable和inInputShareable。上面注释看,
inPurgeable能使bitmap的内存分配到ashmem上,对于
通过filedescriptor去decode的方式,还要设置
inInputShareable为true,只能够使内存分配到ashmem上
。先关注
inPurgeable
/**
* 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>
*/
public boolean inPurgeable;
2.2
从注释看inPurgeable的作用是,在KITKAT及以下,使用该参数的话,当系统需要回收内存的时候,bitmap的pixels可以被清除。好在的是,
当pixels需要被重新访问的时候(例如bitmap draw或者调用getPixels()的时候),它们又可以重新被decode出来。要重新decode出来的话,自然需要encoded data,encdoded data可能来源于对原始那份encoded data的引用,或者是对原始数据的拷贝。具体是引用或者拷贝,就是根据
inInputShareable来决定的,如果是true那就是引用,不然就是deep copy,但是
inInputShareable
即使设置为true,不同的实现也可能是直接进行deep copy。而大的问题是,
inPurgeable虽然能够避免在Java heap中分配内存,但是牺牲点了UI的流畅性,为什么?因为重新decode的过程是在UI线程进行的,这会导致一定的掉帧问题。
inPurgeable可以引出了两个问题:
- Decode过程是如何根据它来把pixels分配到ashmem?sdk文档中并没有提及到这一点,是不是真的如此?
- Decode为什么会导致drop frames,又该如何解决?
Pixels到ashmem过程
回到
decodeFromEncodedImage,它主要是调用decodeByteArrayAsPurgeable进行decode的过程,为了方便(
因为GingerbreadPurgeableDecoder的decode过程较为复杂,而且存在一定问题,留待后面解析
),我们分析
KitKatPurgeableDecoder的该方法:
/**
* Decodes a byteArray into a purgeable bitmap
*
* @param bytesRef the byte buffer that contains the encoded bytes
* @return
*/
@Override
protected Bitmap decodeByteArrayAsPurgeable(
CloseableReference<PooledByteBuffer> bytesRef,
BitmapFactory.Options options) {
final PooledByteBuffer pooledByteBuffer = bytesRef.get();
final int length = pooledByteBuffer.size();
final CloseableReference<byte[]> encodedBytesArrayRef = mFlexByteArrayPool.get(length);
try {
final byte[] encodedBytesArray = encodedBytesArrayRef.get();
pooledByteBuffer.read(0, encodedBytesArray, 0, length);
Bitmap bitmap = BitmapFactory.decodeByteArray(
encodedBytesArray,
0,
length,
options);
return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null");
} finally {
CloseableReference.closeSafely(encodedBytesArrayRef);
}
}
3.1
很简单,就是就是copy数据到byte[],然后调用BitmapFactory的decodeByteArray进行decode。BitmapFactory的decodeByteArray最终通过jni的方式,调用BitmapFactory.cpp的nativeDecodeByteArray方法:
static jobject nativeDecodeByteArray(JNIEnv* env, jobject, jbyteArray byteArray,
int offset, int length, jobject options) {
/* If optionsShareable() we could decide to just wrap the java array and
share it, but that means adding a globalref to the java array object
and managing its lifetime. For now we just always copy the array's data
if optionsPurgeable(), unless we're just decoding bounds.
*/
bool purgeable = optionsPurgeable(env, options) && !optionsJustBounds(env, options);
AutoJavaByteArray ar(env, byteArray);
SkMemoryStream* stream = new SkMemoryStream(ar.ptr() + offset, length, purgeable);
SkAutoUnref aur(stream);
return doDecode(env, stream, NULL, options, purgeable);
}
3.2
从上面代码看,除非BitmapOptions设置了inJustDecodeBounds为true,不然由于
inPurgeable为true,所以临时变量
purgeable都为true。
purgeable的值影响的是SkMemoryStream的区别:
SkMemoryStream::SkMemoryStream(const void* src, size_t size, bool copyData) {
fData = newFromParams(src, size, copyData);
fOffset = 0;
}
static SkData* newFromParams(const void* src, size_t size, bool copyData) {
if (copyData) {
return SkData::NewWithCopy(src, size);
} else {
return SkData::NewWithProc(src, size, NULL, NULL);
}
}
SkData* SkData::NewWithCopy(const void* data, size_t length) {
if (0 == length) {
return SkData::NewEmpty();
}
void* copy = sk_malloc_throw(length); // balanced in sk_free_releaseproc
memcpy(copy, data, length);
return new SkData(copy, length, sk_free_releaseproc, NULL);
}
SkData* SkData::NewWithProc(const void* data, size_t length,
ReleaseProc proc, void* context) {
return new SkData(data, length, proc, context);
}
SkData::SkData(const void* ptr, size_t size, ReleaseProc proc, void* context) {
fPtr = ptr;
fSize = size;
fReleaseProc = proc;
fReleaseProcContext = context;
}
3.3
从上面代码看,区别就是如果是true,则copy一份数据,不然纯粹是引用。我们这里肯定是true,所以固定会copy一份数据,也就是copy一份encoded data。这内存的分配是在native heap上做的。
另外这个刚好反应上面说的,“
但是
inInputShareable
即使设置为true,不同的实现也可能是直接进行deep copy
”
。
再看关键的doDecode方法,由于这个方法很长,只截取相关的一部分去看:
// since we "may" create a purgeable imageref, we require the stream be ref'able
// i.e. dynamically allocated, since its lifetime may exceed the current stack
// frame.
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false) {
int sampleSize = 1;
SkImageDecoder::Mode mode = SkImageDecoder::kDecodePixels_Mode;
SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config;
bool doDither = true;
bool isMutable = false;
float scale = 1.0f;
bool isPurgeable = forcePurgeable || (allowPurgeable && optionsPurgeable(env, options));
bool preferQualityOverSpeed = false;
bool requireUnpremultiplied = false;
jobject javaBitmap = NULL;//其实就是BitmapOptions的inBitmap
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (optionsJustBounds(env, options)) {
mode = SkImageDecoder::kDecodeBounds_Mode;
}
- ........................................
//根据部分参数计算scale的的值(我们这里scale设置为1)
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;
}
}
}
const bool willScale = scale != 1.0f;
isPurgeable &= !willScale;//如果bitmap需要scale那么isPurgeable必然为false
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
if (decoder == NULL) {
return nullObjectReturn("SkImageDecoder::Factory returned null");
}
- ...........................................................
SkAutoTDelete<SkBitmap> adb(outputBitmap == NULL ? new SkBitmap : NULL);
if (outputBitmap == NULL) outputBitmap = adb.get();
NinePatchPeeker peeker(decoder);
decoder->setPeeker(&peeker);
//如果isPurgeable,使用kDecodeBounds_Mode,用于先计算出图片的宽高等,而不是直接decode出pixels来
SkImageDecoder::Mode decodeMode = isPurgeable ? SkImageDecoder::kDecodeBounds_Mode : mode;
- ...........................................................................
// Only setup the decoder to be deleted after its stack-based, refcounted
// components (allocators, peekers, etc) are declared. This prevents RefCnt
// asserts from firing due to the order objects are deleted from the stack.
SkAutoTDelete<SkImageDecoder> add(decoder);
AutoDecoderCancel adc(options, decoder);
// To fix the race condition in case "requestCancelDecode"
// happens earlier than AutoDecoderCancel object is added
// to the gAutoDecoderCancelMutex linked list.
if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) {
return nullObjectReturn("gOptions_mCancelID");
}
SkBitmap decodingBitmap;
//使用kDecodeBounds_Mode,先计算出图片的宽高等,而不是直接decode出pixels来
if (!decoder->decode(stream, &decodingBitmap, prefConfig, decodeMode)) {
return nullObjectReturn("decoder->decode returned false");
}
- .............................................
if (willScale) {
.............
.............
} else {
//把outputBitmap设置为刚才decode出的decodingBitmap
outputBitmap->swap(decodingBitmap);
}
SkPixelRef* pr;
if (isPurgeable) {
pr = installPixelRef(outputBitmap, stream, sampleSize, doDither);
} else {
// if we get here, we're in kDecodePixels_Mode and will therefore
// already have a pixelref installed.
pr = outputBitmap->pixelRef();
}
if (pr == NULL) {
return nullObjectReturn("Got null SkPixelRef");
}
if (!isMutable && javaBitmap == NULL) {
// promise we will never change our pixels (great for sharing and pictures)
pr->setImmutable();
}