Fresco源码详解

通过Fresco加载图片,最简单的方式是通过SimpleDraweeView来展示图片,示例代码如下:

1
2
3
4
5
6
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/my_image_view"
android:layout_width="130dp"
android:layout_height="130dp"
fresco:placeholderImage="@drawable/my_drawable"
/>
1
2
3
4
//示例代码在Fragment#onViewCreated中调用
Uri uri = Uri.parse("https://frescolib.org/static/sample-images/animal_a.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);

先看一下SimpleDraweeView相关的类图结构:

ImageViewDraweeViewGenericDraweeHierarchyGenericDraweeViewSimpleDraweeViewDraweeHierarchygetTopLevelDrawable() : DrawableSettableDraweeHierarchyGenericDraweeHierarchyDraweeControllerDraweeHolderGenericDraweeHierarchyVisibilityCallbackRootDrawableAbstractDraweeControllerPipelineDraweeController

图片加载过程

这个时序图是以文章开头的SimpleDraweeView的示例代码为例进行绘制。

SimpleDraweeViewSimpleDraweeViewPipelineDraweeControllerDraweeHolderDraweeHolderRootDrawableRootDrawableImagePipelineImagePipelineDataSourcesetImageURI();Framework.onViewCreated中调用构造PipelineDraweeControlleronAttachedToWindow(父类方法)onAttachonAttach此时还未设置visible,不会调用attachControlleronVisibilityAggregated(父类ImageView的方法)setVisibleonVisibilityChangeattachControlleronAttachsubmitRequestgetCachedImage获取缓存BitmapgetDataSourcefetchDecodedImage生成生产者序列submitFetchRequest()构造时传入生产者队列DataSource生产者队列启动执行返回实例返回 DataSource<CloseableReference<CloseableImage>>  subscribe(DataSubscriber)注册订阅者返回图片Bitmap。父类的onNewResultInternal()会被执行createDrawablehierarchy.setImage()显示图片

上图中DateSource是一个CloseableProducerToDataSourceAdapter实例,如下图类图所示:

DataSourceCloseableReference<CloseableImage>ProducerCloseableReference<CloseableImage>void produceResults(Consumer<T>, ProducerContext)AbstractDataSourceCloseableReference<CloseableImage>AbstractProducerToDataSourceAdapterCloseableReference<CloseableImage>CloseableProducerToDataSourceAdapterCloseableImageDataSubscriberCloseableReference<CloseableImage>依赖构造函数中调用1n

Producer就是图片的生产者队列,会执行网络请求、缓存读写、图片解码等等图片处理任务。下文会详细介绍生产者队列。

PipelineDraweeController的构造过程

SimpleDraweeViewSimpleDraweeViewPipelineDraweeControllerBuilderSupplierPipelineDraweeControllerBuilderSupplierPipelineDraweeControllerBuilderPipelineDraweeControllerinitget()PipelineDraweeControllerBuilder返回PipelineDraweeControllerBuildersetImageURIsetUri()setOldController()调用父类build()调用父类buildController()obtainController()newPipelineDraweeController调用父类obtainDataSourceSupplier(),返回一个内部类Supplier<DataSource<T>>返回PipelineDraweeController

上图中的Supplier是Supplier<DataSource<CloseableReference<CloseableImage>>>是一个内部类,这个类就是DataSource提供者。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//AbstractDraweeControllerBuilder.java
protected Supplier<DataSource<IMAGE>> getDataSourceSupplierForRequest(
final REQUEST imageRequest,
final boolean bitmapCacheOnly) {
final Object callerContext = getCallerContext();
return new Supplier<DataSource<IMAGE>>() {
@Override
public DataSource<IMAGE> get() {
return getDataSourceForRequest(imageRequest, callerContext, bitmapCacheOnly);
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("request", imageRequest.toString())
.toString();
}
};
}
//PipelineDraweeControllerBuilder.java
protected DataSource<CloseableReference<CloseableImage>> getDataSourceForRequest(
DraweeController controller,
String controllerId,
ImageRequest imageRequest,
Object callerContext,
AbstractDraweeControllerBuilder.CacheLevel cacheLevel) {
return mImagePipeline.fetchDecodedImage(
imageRequest,
callerContext,
convertCacheLevelToRequestLevel(cacheLevel),
getRequestListener(controller),
controllerId);
}

下面是与PipelineDraweeController构造相关的类图:

PipelineDraweeControllerBuilderSupplierPipelineDraweeControllerBuilder get()单例ImagePipelinePipelineDraweeControllerBuilderPipelineDraweeControllerFactoryAbstractDraweeControllerBuildergetDataSourceSupplierForRequest()SupplierDataSource<CloseableReference<CloseableImage>>构造构造内部类返回

三级缓存机制

Fresco采用了三级缓存机制,两级内存,一级磁盘。

Bitmap内存缓存原始图片内存缓存磁盘缓存MemoryCacheCacheKey, CloseableImageCountingMemoryCacheCacheKey, CloseableImageLruCountingMemoryCacheCacheKey, CloseableImageMemoryCacheCacheKey, PooledByteBufferCountingMemoryCacheCacheKey, PooledByteBufferLruCountingMemoryCacheCacheKey, PooledByteBufferBufferedDiskCacheFileCacheDiskStorageCacheImagePipelinemBitmapMemoryCachemEncodedMemoryCachemMainBufferedDiskCachemSmallImageBufferedDiskCache

内存缓存池

Fresco的第一级和第二级内存缓存都是采用LruCountingMemoryCache来进行内存管理。

LruCountingMemoryCacheCountingLruMap<K, Entry<K, V>> mExclusiveEntries;CountingLruMap<K, Entry<K, V>> mCachedEntries;CountingLruMapK, Entry<K, V>LinkedHashMap<K, V> mMap;int mSizeInBytes = 0;CountingLruMapK, Entry<K, V>LinkedHashMap<K, V> mMap;int mSizeInBytes = 0;LinkedHashMapK, VLinkedHashMapK, V

LruCountingMemoryCache包含了两个CountingLruMap:mCachedEntries保存所有缓存数据,即缓存池;mExclusiveEntries是回收队列,用于保存不再被使用的数据,这些数据是可以被安全回收的。CountingLruMap中的数据通过LinkedHashMap来保存,同时CountingLruMap中有一个mSizeInBytes变量,表示缓存数据的总大小,在每次添加和删除数据时都会更新mSizeInBytes。

缓存池中保存的是Entry<K, V>数据类型,然而添加和获取缓存数据都会通过newClientReference方法来创建一个CloseableReference<V>返回的是一个CloseableReference<V>类型的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//LruCountingMemoryCache.java
private synchronized CloseableReference<V> newClientReference(final Entry<K, V> entry) {
increaseClientCount(entry);
return CloseableReference.of(
entry.valueRef.get(),
new ResourceReleaser<V>() {
@Override
public void release(V unused) { //当CloseableReference#close被调用时就会执行release方法
releaseClientReference(entry);
}
});
}

/** Called when the client closes its reference. */
private void releaseClientReference(final Entry<K, V> entry) {
Preconditions.checkNotNull(entry);
boolean isExclusiveAdded;
CloseableReference<V> oldRefToClose;
synchronized (this) {
decreaseClientCount(entry);
isExclusiveAdded = maybeAddToExclusives(entry);
oldRefToClose = referenceToClose(entry);
}
CloseableReference.closeSafely(oldRefToClose);
maybeNotifyExclusiveEntryInsertion(isExclusiveAdded ? entry : null);
maybeUpdateCacheParams();
maybeEvictEntries();
}

/** Adds the entry to the exclusively owned queue if it is viable for eviction. */
private synchronized boolean maybeAddToExclusives(Entry<K, V> entry) {
if (!entry.isOrphan && entry.clientCount == 0) {
mExclusiveEntries.put(entry.key, entry); //添加到回收队列中
return true;
}
return false;
}

当数据消费者不再使用这条数据时,CloseableReference#close()就会被调用,进而调用到上面代码中的release回调方法,当这条数据的使用者数量为零时,就会把这条数据添加到mExclusiveEntries回收队列中,mCachedEntries依然会保留这条数据。当从缓存池get数据时,如果这条数据在回收队列中,将会从回收队列中移除掉,因为这条数据有了使用者,就不能被回收了。

再来看下缓存池的配置参数:

  • maxCacheSize 缓存的最大大小,以字节为单位;
  • maxCacheEntries 缓存中能够保存的最大数量;
  • maxEvictionQueueSize 回收队列用于存储准备回收的数据,这个字段表示回收队列的最大字节数;
  • maxEvictionQueueEntries 回收队列可以保存的最大数量;
  • maxCacheEntrySize 单个缓存数据的最大内存;
  • paramsCheckIntervalMs 检查配置参数更新的间隔时间(以毫秒为单位)。当添加缓存、获取缓存等操作时,都会检查距离上次更新配置参数是否超过这个值,如果超过了,就会重新后去这些配置参数。

上面这些参数用于确定缓存池的最大容量,当超过容量限制时就会触发回收动作,回收时只会删除已经在回收队列中的数据,不在回收队列中的数据说明还再被使用,是不能回收的。在添加、获取缓存数据时都会调用maybeEvictEntries来判断是否要对缓存池进行清理回收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void maybeEvictEntries() {
ArrayList<Entry<K, V>> oldEntries;
synchronized (this) {
//计算回收队列要保留的数据个数
int maxCount =
Math.min(
mMemoryCacheParams.maxEvictionQueueEntries,
mMemoryCacheParams.maxCacheEntries - getInUseCount());
//计算回收队列要保留数据的总大小
int maxSize =
Math.min(
mMemoryCacheParams.maxEvictionQueueSize,
mMemoryCacheParams.maxCacheSize - getInUseSizeInBytes());
oldEntries = trimExclusivelyOwnedEntries(maxCount, maxSize);
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
maybeNotifyExclusiveEntryRemoval(oldEntries);
}
//获取正在被使用的数据的个数
public synchronized int getInUseCount() {
return mCachedEntries.getCount() - mExclusiveEntries.getCount();
}

//执行回收工作
private synchronized ArrayList<Entry<K, V>> trimExclusivelyOwnedEntries(int count, int size) {
count = Math.max(count, 0);
size = Math.max(size, 0);
// fast path without array allocation if no eviction is necessary
if (this.mExclusiveEntries.getCount() <= count && mExclusiveEntries.getSizeInBytes() <= size) {
return null;
}
ArrayList<Entry<K, V>> oldEntries = new ArrayList<>();
//循环从回收队列删除数据,直到同时满足小于等于size和count
while (this.mExclusiveEntries.getCount() > count || mExclusiveEntries.getSizeInBytes() > size) {
@Nullable K key = mExclusiveEntries.getFirstKey(); //获取回收队列中最早添加的数据
......
mExclusiveEntries.remove(key); //从回收队列中删除
oldEntries.add(mCachedEntries.remove(key)); //从缓存池删除
}
return oldEntries;
}

内存块缓存池-MemoryChunkPool

MemoryChunkPoolBufferMemoryChunkPoolBufferMemoryChunkAshmemMemoryChunkPoolNativeMemoryChunkPoolNativeMemoryChunkMemoryChunkAshmemMemoryChunkBasePoolMemoryChunkSparseArray<Bucket<MemoryChunk>> mBucketsBucketMemoryChunkQueue<MemoryChunk> mFreeListnewnewnew1n1n

Fresco默认使用NativeMemoryChunkPool,而如果要使用AshmemMemoryChunkPool则必须配置开发打开,并且8.1及以上的android版本才能使用(ImagePipelineConfig#getMemoryChunkType)。

  • NativeMemoryChunk使用nativce内存,通过C函数malloc来申请内存;
  • AshmemMemoryChunk使用匿名共享内存,通过android.os.SharedMemory#create来申请内存;
  • BufferMemoryChunk使用Java堆上的内存,通过java.nio.ByteBuffer#allocateDirect来申请内存;

MemoryChunkPool中维护了一个int数组(mBucketSizes),是内存块大小列表,数组的值来自PoolParams#bucketSizes的keys,此处不会使用其value值。PoolParams#bucketSizes是个SparseIntArray类型,相当于map类型,key、value都是int。key表示内存块大小、value表示该大小的内存块的个数。默认值如下默认值在DefaultNativeMemoryChunkPoolParams#get赋值

bucketSizes key=>value1024 (1KB)52048 (2KB)54096 (4KB)58192 (8KB)516384 (16KB)532768 (32KB)565536 (64KB)5131072 (128KB)5262144 (256KB)2524288 (512KB)21048576 (1MB)2

当需要获取内存块时,会调用MemoryChunkPool#get(int size)来获取一个MemoryChunk。比如要获取一个60KB大小的内存块,就会在mBucketSizes数组中找大于等于60KB的最小值,所以,最终内存块大小是64KB。申请内存块时,如果内存池中已经有对应大小的内存块就返回,否则会创建一个新的内存块,并申请内存。

Bucket<MemoryChunk>中维护的都是相同大小的内存块,使用一个Queue维护空闲的内存块,当内存块被取走后,就在Queue中删除了。当内存块使用完后再放入Queue中。

使用场景:

  • NetworkFetchProducer中,图片从网络下载后,原始图片数据就会写入到一个MemoryChunk中。

字节内存缓存池 ByteArrayPool

BasePoolByteArrayPoolbyte []ByteArrayPoolGenericByteArrayPool

字节缓存池与内存块缓存池的机制是一样的,区别是内存块缓存池使用了1KB、2KB、4KB等一系列大小的内存块,而字节内存缓存池只有16KB大小的一种缓存块。一般作为一个临时内存来使用,使用完成后立马放回缓存池,

使用场景:

  • 网络请求数据从输入流中读数据到一个临时的buffer中,这个buffer就是从字节内存缓存池中拿到的;
  • 图片缓存到磁盘中时,内存的输入流中先读取数据到一个buffer中,然后从buffer写入到磁盘的输出流,其中buffer也是从字节内存缓存池中拿到;

图片解码器

ImageDecoderDefaultImageDecoderImageDecoder mAnimatedGifDecoderImageDecoder mAnimatedWebPDecoderPlatformDecoder mPlatformDecoderImageDecoder mDefaultDecoder内部类,根据图片格式选择DecoderPlatformDecoderDefaultDecoderOreoDecoderArtDecoderPlatformDecoderFactoryGingerbreadPurgeableDecoderKitKatPurgeableDecoderDalvikPurgeableDecoderBitmapPool

Bitmap内存占用

  • Android 2.3.3 (API level 10)以及更早的Android版本中,Bitmap的像素数据保存在native内存中,像素数据内存的回收则在finalize()中进行回收,存在很大的不确定性,很容易导致OOM的发生;
  • 从Android 3.0 (API level 11) 到Android 7.1 (API level 25),像素数据存放在Java Heap中,跟Bitmap对象一起回收。但由于图片是内存消耗大户,所以也很容易导致OOM,以及频繁的GC导致内存抖动问题。
  • 在Android 8.0 (API level 26)以及更高的版本中,像素数据保存在native heap中。通过一个辅助类NativeAllocationRegistry来实现native内存的回收。

Android5.0及以上的版本,Fresco采用标准的Bitmap解码,通过BitmapFactoty.decodeXXX系列函数进行位图解码,pixel数据内存的分配完全由Android系统决定,可以参考Bitmap内存分配以及回收,所以这里就不介绍了。

本节重点介绍在Android5.0以下的系统上,Fresco是如何把pixel数据放在了共享内存中的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//imagepipeline-native/src/main/java/com/facebook/imagepipeline/nativecode/DalvikPurgeableDecoder.java
public CloseableReference<Bitmap> decodeJPEGFromEncodedImageWithColorSpace(
final EncodedImage encodedImage,
Bitmap.Config bitmapConfig,
@Nullable Rect regionToDecode,
int length,
@Nullable final ColorSpace colorSpace) {

//设置解码配置信息,函数实现见下方代码
BitmapFactory.Options options =
getBitmapFactoryOptions(encodedImage.getSampleSize(), bitmapConfig);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
OreoUtils.setColorSpace(options, colorSpace);
}
final CloseableReference<PooledByteBuffer> bytesRef = encodedImage.getByteBufferRef();
Preconditions.checkNotNull(bytesRef);
try {
//最终会调用BitmapFactory.decodeByteArray或BitmapFactory.decodeFileDescriptor来解码位图
Bitmap bitmap = decodeJPEGByteArrayAsPurgeable(bytesRef, length, options);

//通过jni最终调用framework中的AndroidBitmap_lockPixels(c++函数),待会儿再介绍作用
return pinBitmap(bitmap);
} finally {
CloseableReference.closeSafely(bytesRef);
}
}

public 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
//BitmapFactory.cpp中doDecode时会读取这个配置,从而使用共享内存
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;
options.inMutable = true; // no known perf difference; allows postprocessing to work
return options;
}

继续看以下android framework 中bitmap解码的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//android-4.4.4_r1\frameworks\base\core\jni\android\graphics\BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false) {
...... //代码省略
bool isPurgeable = forcePurgeable || (allowPurgeable && optionsPurgeable(env, options));
......
const bool willScale = scale != 1.0f;
isPurgeable &= !willScale; //如果配置了按比例缩放,则isPurgeable不生效
......
//如果配置了isPurgeable,将会使用kDecodeBounds_Mode解码模式,即只解析长宽等信息,不会申请piexel内存
SkImageDecoder::Mode decodeMode = isPurgeable ? SkImageDecoder::kDecodeBounds_Mode : mode;

JavaPixelAllocator javaAllocator(env);
RecyclingPixelAllocator recyclingAllocator(outputBitmap->pixelRef(), existingBufferSize);
ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
(SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
//isPurgeable为true时,不会走入这个if判断,所以decoder不会设置allocator
if (!willScale) {
// If the java allocator is being used to allocate the pixel memory, the decoder
// need not write zeroes, since the memory is initialized to 0.
decoder->setSkipWritingZeroes(outputAllocator == &javaAllocator);
decoder->setAllocator(outputAllocator);
} else if (javaBitmap != NULL) {
// check for eventual scaled bounds at allocation time, so we don't decode the bitmap
// only to find the scaled result too large to fit in the allocation
decoder->setAllocator(&scaleCheckingAllocator);
}
}
......
SkBitmap decodingBitmap;
//使用SkImageDecoder::kDecodeBounds_Mode模式进行解码,所以decodingBitmap中没有pixel数据
if (!decoder->decode(stream, &decodingBitmap, prefConfig, decodeMode)) {
return nullObjectReturn("decoder->decode returned false");
}
......
// if we're in justBounds mode, return now (skip the java bitmap)
//此处并没有使用decodeMode来判断,所以不会返回,还会继续执行后面的代码
if (mode == SkImageDecoder::kDecodeBounds_Mode) {
return NULL;
}
......
SkPixelRef* pr;
if (isPurgeable) {
//installPixelRef函数实现见下文
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");
}
......
// now create the java bitmap
return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),
bitmapCreateFlags, ninePatchChunk, layoutBounds, -1);
}

static SkPixelRef* installPixelRef(SkBitmap* bitmap, SkStreamRewindable* stream,
int sampleSize, bool ditherImage) {

SkImageInfo bitmapInfo;
if (!bitmap->asImageInfo(&bitmapInfo)) {
ALOGW("bitmap has unknown configuration so no memory has been allocated");
return NULL;
}

SkImageRef* pr;
// only use ashmem for large images, since mmaps come at a price
//当Bitmap内存超过32K时,才会使用共享内存
if (bitmap->getSize() >= 32 * 1024) {
pr = new SkImageRef_ashmem(bitmapInfo, stream, sampleSize);
} else {
//会走到SkBitmap::allocPixels默认申请内存native内存的分支,并通过一个全局缓存池进行管理
pr = new SkImageRef_GlobalPool(bitmapInfo, stream, sampleSize);
}
pr->setDitherImage(ditherImage);
bitmap->setPixelRef(pr)->unref();
pr->isOpaque(bitmap);
return pr;
}

从上面代码可以看出,当isPurgeable==true时,并没有解码pixel数据,而且内存也没有申请,那么什么时候进行解码呢?还记得Fresco调用BitmapFactory解码后有个pinBitmap的函数调用吧,这个函数通过jni调用AndroidBitmap_lockPixels函数,进而调用到SkImageRef::onLockPixels,从而触发pixel解码。下面代码介绍了触发解码的过程,以及共享内存的分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//android-4.4.4_r1\external\skia\src\images\SkImageRef.cpp
//Fresco的pinBitmap最终就会调用这个函数,进而触发pixel解码,pixel数据存放在共享内存
void* SkImageRef::onLockPixels(SkColorTable** ct) {
if (NULL == fBitmap.getPixels()) {
(void)this->prepareBitmap(SkImageDecoder::kDecodePixels_Mode);
}

if (ct) {
*ct = fBitmap.getColorTable();
}
return fBitmap.getPixels();
}
//SkImageRef_ashmem就是SkImageRef的子类,prepareBitmap会调用onDecode
bool SkImageRef::prepareBitmap(SkImageDecoder::Mode mode) {
......
SkImageDecoder* codec;
if (fFactory) { //默认是null,不走这个分支
codec = fFactory->newDecoder(fStream);
} else {
codec = SkImageDecoder::Factory(fStream);
}

if (codec) {
SkAutoTDelete<SkImageDecoder> ad(codec);

codec->setSampleSize(fSampleSize);
codec->setDitherImage(fDoDither);

//此处就是SkImageRef_ashmem::onDecode,见下面函数定义
if (this->onDecode(codec, fStream, &fBitmap, fBitmap.config(), mode)) {
SkDEBUGCODE(SkImageInfo info;)
SkASSERT(!fBitmap.asImageInfo(&info) || this->info().fColorType == info.fColorType);
SkASSERT(this->info().fWidth == fBitmap.width());
SkASSERT(this->info().fHeight == fBitmap.height());
return true;
}
}
......
}

//android-4.4.4_r1\external\skia\src\images\SkImageRef_ashmem.cpp
bool SkImageRef_ashmem::onDecode(SkImageDecoder* codec, SkStreamRewindable* stream,
SkBitmap* bitmap, SkBitmap::Config config,
SkImageDecoder::Mode mode) {

if (SkImageDecoder::kDecodeBounds_Mode == mode) {
return this->INHERITED::onDecode(codec, stream, bitmap, config, mode);
}

// Ashmem memory is guaranteed to be initialized to 0.
codec->setSkipWritingZeroes(true);

//这就是我们的主角,共享内存分配器
AshmemAllocator alloc(&fRec, this->getURI());

//向解码器中注册共享内存分配器。如果不注册将会采用默认的内存分配器
codec->setAllocator(&alloc);

//调用父类的onDecode,父类会调用codec->decode进行位图解码,见下面函数定义
bool success = this->INHERITED::onDecode(codec, stream, bitmap, config,
mode);
// remove the allocator, since its on the stack
codec->setAllocator(NULL);

if (success) {
// remember the colortable (if any)
SkRefCnt_SafeAssign(fCT, bitmap->getColorTable());
return true;
} else {
if (fRec.fPinned) {
ashmem_unpin_region(fRec.fFD, 0, 0);
fRec.fPinned = false;
}
this->closeFD();
return false;
}
}

bool SkImageRef::onDecode(SkImageDecoder* codec, SkStreamRewindable* stream,
SkBitmap* bitmap, SkBitmap::Config config,
SkImageDecoder::Mode mode) {
return codec->decode(stream, bitmap, config, mode);
}

//android-4.4.4_r1\external\skia\src\images\SkImageDecoder_libjpeg.cpp
bool SkJPEGImageDecoder::onDecode(SkStream* stream, SkBitmap* bm, Mode mode) {
......
if (SkImageDecoder::kDecodeBounds_Mode == mode) {
return true;
}
if (!this->allocPixelRef(bm, NULL)) { //调用父类函数
return return_false(cinfo, *bm, "allocPixelRef");
}
......
}

//android-4.4.4_r1\external\skia\src\core\SkBitmap.cpp
bool SkImageDecoder::allocPixelRef(SkBitmap* bitmap,
SkColorTable* ctable) const {
//调用bitmap分配pixel内存,fAllocator就是前面我们设置的共享内存分配器
return bitmap->allocPixels(fAllocator, ctable);
}

bool SkBitmap::allocPixels(Allocator* allocator, SkColorTable* ctable) {
//这是默认内存分配器,因为我们传入了allocator参数,所以不会使用默认内存分配器
//上面代码中介绍的SkImageRef_GlobalPool就会使用这个默认的内存分配器
HeapAllocator stdalloc;

if (NULL == allocator) {
allocator = &stdalloc;
}
//调用AshmemAllocator::allocPixelRef来分配内存
return allocator->allocPixelRef(this, ctable);
}

class AshmemAllocator : public SkBitmap::Allocator {
public:
AshmemAllocator(SkAshmemRec* rec, const char name[])
: fRec(rec), fName(name) {}

virtual bool allocPixelRef(SkBitmap* bm, SkColorTable* ct) {
const size_t size = roundToPageSize(bm->getSize());
int fd = fRec->fFD;
void* addr = fRec->fAddr;

SkASSERT(!fRec->fPinned);

if (-1 == fd) {
SkASSERT(NULL == addr);
SkASSERT(0 == fRec->fSize);

//打开/dev/ashmem虚拟设备,然后ioctl配置name和size
fd = ashmem_create_region(fName, size);
......

//调用mmap分配内存
addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
......

fRec->fFD = fd;
fRec->fAddr = addr;
fRec->fSize = size;
} else {
SkASSERT(addr);
SkASSERT(size == fRec->fSize);
(void)ashmem_pin_region(fd, 0, 0);
}

bm->setPixels(addr, ct);
fRec->fPinned = true;
return true;
}

private:
// we just point to our caller's memory, these are not copies
SkAshmemRec* fRec;
const char* fName;
};

Bitmap复用

通过android.graphics.BitmapFactory#decodeStream(...)接口进行图片解码时可以通过设置BitmapFactory.Options#inBitmap来复用旧的Bitmap实例,这样可以避免频繁申请内存。但是有如下限制条件:

  1. 被复用的Bitmap的isMutable属性必须为true;
  2. Android4.4及以上版本,只要新图片需要的pixels内存小于等于inBitmap的即可;
  3. Android4.4以下版本有额外的限制,图片必须是jpeg或png格式,而且pixels内存大小必须相等,且inSampleSize必须是1;而且新图片的inPreferredConfig属性将会被inBitmap覆盖。

解码时Bitmap的复用代码在DefaultDecoder#decodeFromStream中。被复用的Bitmap保存在BitmapPool中。当一个Bitmap不再被使用后(引用计数为0)就会被放到BitmapPool中:

PoolBitmapBitmapPoolBasePoolBitmapBucketsBitmapPoolDummyBitmapPoolLruBitmapPoolPoolFactoryBitmapPool getBitmapPool()BucketBitmapBitmap1n1n

BitmapPool默认情况下使用BucketsBitmapPool和DummyBitmapPool,Android5.0及以上使用BucketsBitmapPool。

Bitmap是如何被放到BitmapPool中的呢?DefaultDecoder.decodeFromStream生成Bitmap后并不是直接返回一个Bitmap,而是一个CloseableReference,CloseableReference就会把Bitmap与BitmapPool进行关联,代码如下:

1
2
3
4
5
6
7
8
9
//DefaultDecoder.java
private CloseableReference<Bitmap> decodeFromStream(
InputStream inputStream,
BitmapFactory.Options options,
@Nullable Rect regionToDecode,
@Nullable final ColorSpace colorSpace) {
...... //代码省略
return CloseableReference.of(decodedBitmap, mBitmapPool);
}

每次CloseableReference#close都会导致Bitmap的引用计数减一,当计数为零后,就会调用到BitmapPool#release,把Bitmap放到BitmapPool中。release的含义就是把不再使用的Bitmap放到缓存池中备用。

CloseableReferenceBitmapvoid close()SharedReferenceBitmapint mRefCount //引用计数deleteReference()ResourceReleaserBitmapvoid release(Bitmap v)BitmapPoolvoid release(Bitmap v)Bitmap调用当引用计数为0时调用release把Bitmap释放到缓存池中

什么时候引用计数加一呢? 调用CloseableReference子类clone方法克隆一个新的CloseableReference时,就等价于增加了一个使用者,应用计数就会加一。 新和旧的CloseableReference持有同一个SharedReference实例。

BitmapPool 不会无限扩张,最大支持多少个Bitmap?是否有清理策略?

SharedReferenceBitmapBitmap mValueint mRefCountResourceReleaser<Bitmap> mResourceReleaserBitmapCloseableReferenceBitmapDefaultCloseableReferenceBitmapCloseableImageCloseableBitmapCloseableStaticBitmapBaseCloseableImageBaseCloseableStaticBitmapDefaultCloseableStaticBitmap

com.facebook.imagepipeline.memory.DefaultNativeMemoryChunkPoolParams#get

解码的临时缓存

DefaultDecoder在解码图片生成BitMap时可以设置临时缓存,防止频繁GC,通过android.graphics.BitmapFactory.Options#inTempStorage来设置图片解码临时缓存,api文档中临时缓存大小建议为16K左右。临时缓存使用Pools.Pool<ByteBuffer>,是SynchronizedPool,缓存的个数默认是java虚拟机可使用的处理器个数(可等同于cpu的核心数,但不是完全对等)。这个是解码器独用的内存池,在Java堆上申请内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// PlatformDecoderFactory.kt
fun createPool(poolFactory: PoolFactory, useDecodeBufferHelper: Boolean): Pools.Pool<ByteBuffer> {
if (useDecodeBufferHelper) {
return DecodeBufferHelper.INSTANCE
}
val maxNumThreads = poolFactory.flexByteArrayPoolMaxNumThreads
val pool: Pools.Pool<ByteBuffer> = SynchronizedPool(maxNumThreads)
for (i in 0 until maxNumThreads) {
//release这个函数含义是:将实例释放到缓存池中,提供给后续使用。
//每个buffer的默认大小是16K
pool.release(ByteBuffer.allocate(DecodeBufferHelper.getRecommendedDecodeBufferSize()))
}
return pool
}

图片处理生产者序列

Fresco从一个图片的URL下载图片并解码成Bitmap需要经过一个生产者序列的处理。在ImagePipeline#fetchDecodedImage中开始生成队列并执行任务序列。下图展示了图片处理的所有生产者,其中有些序列默认是不使用的,除非进行特别配置或者特定场景。图中箭头的方向是图片数据流动的方向,而调用顺序正好相反,后面的Producer持有前面的Producer,当然这个持有只是对Producer接口的依赖,并不是直接依赖实现类。

图片数据NetworkFetchProducerWebpTranscodeProducerPartialDiskCacheProducerDiskCacheWriteProducerDiskCacheReadProducerEncodedMemoryCacheProducerEncodedProbeProducerEncodedCacheKeyMultiplexProducerAddImageTransformMetaDataProducerResizeAndRotateProducerDecodeProducerBitmapMemoryCacheProducerBitmapMemoryCacheKeyMultiplexProducerThreadHandoffProducerBitmapMemoryCacheGetProducerBitmapProbeProducerPostprocessorProducerPostprocessedBitmapMemoryCacheProducerBitmapPrepareProducerDelayProducerBitmap

NetworkFetchProducer

Fresco默认使用HttpURLConnection而不是Okhttp来请求网络图片,在生成ImagePipelineConfig时需要使用OkHttpImagePipelineConfigFactory来使用Okhttp来请求网络。

ProducerEncodedImageNetworkFetchProducerNetworkFetcherMemoryChunkPoolByteArrayPoolBaseNetworkFetcherHttpUrlConnectionNetworkFetcherOkHttpNetworkFetcherMemoryPooledByteBufferFactorynewOutputStream()MemoryPooledByteBufferOutputStreamMemoryPooledByteBuffer toByteBuffer()MemoryPooledByteBufferMemoryChunkreturnreturntoByteBuffer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//NetworkFetchProducer.java
protected void onResponse(
FetchState fetchState, InputStream responseData, int responseContentLength)
throws IOException {
final PooledByteBufferOutputStream pooledOutputStream;
if (responseContentLength > 0) {
pooledOutputStream = mPooledByteBufferFactory.newOutputStream(responseContentLength);
} else {
pooledOutputStream = mPooledByteBufferFactory.newOutputStream();
}
final byte[] ioArray = mByteArrayPool.get(READ_SIZE);
try {
int length;
//responseData(InputStream)对应okhttp3.ResponseBody#byteStream,原始图片byte数据流
//循环从InputStream中读取数据到ioArray中
while ((length = responseData.read(ioArray)) >= 0) {
if (length > 0) {
//将图片数据写入到pooledOutputStream是MemoryPooledByteBufferOutputStream类型
pooledOutputStream.write(ioArray, 0, length);
maybeHandleIntermediateResult(pooledOutputStream, fetchState);
float progress = calculateProgress(pooledOutputStream.size(), responseContentLength);
fetchState.getConsumer().onProgressUpdate(progress);
}
}
mNetworkFetcher.onFetchCompletion(fetchState, pooledOutputStream.size());
handleFinalResult(pooledOutputStream, fetchState);
} finally {
mByteArrayPool.release(ioArray);
pooledOutputStream.close();
}
}

NetworkFetchProducer从网络请求的图片数据写入到MemoryPooledByteBufferOutputStream中。MemoryPooledByteBufferOutputStream从内存池中申请内存块(详情参考内存块缓存池一节),在OutputStream的close中会把申请的内存块放回到内存池中。

NetworkFetchProducer处理结束时返回一个EncodedImage类型数据,class结构如下,图片数据保存在MemoryChunk中。

PooledByteBufferCloseableReferencePooledByteBufferEncodedImageMemoryPooledByteBufferclose()DefaultCloseableReferencePooledByteBufferSharedReferencePooledByteBufferint mRefCountResourceReleaserPooledByteBufferCloseableCloseableReferenceMemoryChunkMemoryChunkPoolResourceReleaserMemoryChunkrelease()MemoryChunk

DiskCacheWriteProducer

DiskCacheWriteProducer用于缓存原始图片数据,包括两级缓存:内存和磁盘。

DiskCacheWriteProducermDefaultBufferedDiskCachemSmallImageBufferedDiskCacheBufferedDiskCacheStagingAreaMap<CacheKey, EncodedImage> mMapFileCacheBinaryResource getResource(CacheKey key)BinaryResource insert(CacheKey key, WriterCallback writer)DiskStorageCacheDiskStorageDynamicDefaultDiskStorage

DiskCacheWriteProducer接收到下级Producer的EncodedImage后就会调用BufferedDiskCache#put()进行缓存。BufferedDiskCache在缓存图片时,会先把图片(EncodedImage)保存到StagingArea(mMap)中,即内存缓存,下次获取时也会优先从StagingArea中获取内存中的图片缓存。然后,在子线程中写入磁盘缓存。

DiskCacheReadProducer

DiskCacheReadProducermDefaultBufferedDiskCachemSmallImageBufferedDiskCacheBufferedDiskCache

DiskCacheReadProducer也持有两个BufferedDiskCache,与DiskCacheWriteProducer中的同一个对象实例。从DiskCacheWriteProducer的分析中我们知道图片会优先保存到内存中(StagingArea),读取时也优先使用内存中的缓存。

DiskCacheReadProducer启动后会先从缓存读取图片,如果存在缓存的图片就不会再调用下一级Producer,直接把结果返回给上一级。如果没有读取到缓存的图片,就会继续调用下一级Producer。

EncodedMemoryCacheProducer

MemoryCacheCacheKey, PooledByteBufferProducerEncodedImageEncodedMemoryCacheProducer下一级生产者CountingMemoryCacheCacheKey, PooledByteBufferLruCountingMemoryCacheCacheKey, PooledByteBufferCountingMemoryCache.EntryCacheKey, PooledByteBufferCountingLruMapCacheKey, Entryn1

EncodedMemoryCacheProducer执行时,会首先从MemoryCache读取缓存在内存中的图片,如果有缓存,则将缓存的图片传递给上一级生产者,不再执行下一级生产者。

根据CacheKey从缓存中获取图片执行下一级生产者收到下级生产者返回的图片缓存图片不存在缓存图片存在?存在返回图片给上一级生产者

加入缓存或者获取缓存等操作,都会触发缓存的清理,清理时如果超过了最大数量或者最大存储空间,都会进行清理,被清理掉的都是不被使用的图片,而且时最老的优先被清理。

DecodeProducer

ProducerCloseableReference<CloseableImage>DecodeProducerByteArrayPoolImageDecoderExecutor

BitmapMemoryCacheProducer

MemoryCacheCacheKey, CloseableImageCountingMemoryCacheCacheKey, CloseableImageLruCountingMemoryCacheCacheKey, CloseableImageBitmapMemoryCacheProducerMemoryTrimmablevoid trim(MemoryTrimType trimType);

配置参数的默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//imagepipeline/src/main/java/com/facebook/imagepipeline/cache/DefaultBitmapMemoryCacheParamsSupplier.java
public MemoryCacheParams get() {
return new MemoryCacheParams(
getMaxCacheSize(), //maxCacheSize等于APP最大堆内存的四分之一,代码见后面
MAX_CACHE_ENTRIES, //maxCacheEntries = 256
MAX_EVICTION_QUEUE_SIZE, //maxEvictionQueueSize = Integer.MAX_VALUE
MAX_EVICTION_QUEUE_ENTRIES, //maxEvictionQueueEntries = Integer.MAX_VALUE
MAX_CACHE_ENTRY_SIZE, //maxCacheEntrySize = Integer.MAX_VALUE
PARAMS_CHECK_INTERVAL_MS); //paramsCheckIntervalMs = 5 minutes
}
private int getMaxCacheSize() {
//mActivityManager.getMemoryClass()是获取的没有设置android:largeHeap属性的APP的最大堆内存;
//也就是获取系统属性dalvik.vm.heapgrowthlimit的值
final int maxMemory =
Math.min(mActivityManager.getMemoryClass() * ByteConstants.MB, Integer.MAX_VALUE);
if (maxMemory < 32 * ByteConstants.MB) {
return 4 * ByteConstants.MB;
} else if (maxMemory < 64 * ByteConstants.MB) {
return 6 * ByteConstants.MB;
} else {
return maxMemory / 4; //app最大堆内存的四分之一
}
}

从缓存获取Bitmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//LruCountingMemoryCache.java
public CloseableReference<V> get(final K key) {
Preconditions.checkNotNull(key);
Entry<K, V> oldExclusive;
CloseableReference<V> clientRef = null;
synchronized (this) {
oldExclusive = mExclusiveEntries.remove(key); //首先从回收列表移除
Entry<K, V> entry = mCachedEntries.get(key); //从LinkedHashMap中获取缓存的Bitmap
if (entry != null) {
clientRef = newClientReference(entry);
}
}
maybeNotifyExclusiveEntryRemoval(oldExclusive);
maybeUpdateCacheParams(); //查看是否需要更新配置参数,前面已经介绍
maybeEvictEntries();
return clientRef;
}

//如果内存超过最大限制,就会开始清理回收列表中存在的Bitmap
public void maybeEvictEntries() {
ArrayList<Entry<K, V>> oldEntries;
synchronized (this) {
int maxCount =
Math.min(
mMemoryCacheParams.maxEvictionQueueEntries, //maxEvictionQueueEntries默认是Integer.MAX_VALUE
mMemoryCacheParams.maxCacheEntries - getInUseCount()); //maxCacheEntries默认是256
int maxSize =
Math.min(
mMemoryCacheParams.maxEvictionQueueSize, //maxEvictionQueueSize默认值Integer.MAX_VALUE
mMemoryCacheParams.maxCacheSize - getInUseSizeInBytes()); //maxCacheSize是APP最大堆内存的四分之一
oldEntries = trimExclusivelyOwnedEntries(maxCount, maxSize);
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
maybeNotifyExclusiveEntryRemoval(oldEntries);
}
public synchronized int getInUseCount() {
return mCachedEntries.getCount() - mExclusiveEntries.getCount();
}
public synchronized int getInUseSizeInBytes() {
return mCachedEntries.getSizeInBytes() - mExclusiveEntries.getSizeInBytes();
}

private synchronized ArrayList<Entry<K, V>> trimExclusivelyOwnedEntries(int count, int size) {
count = Math.max(count, 0);
size = Math.max(size, 0);
// fast path without array allocation if no eviction is necessary
if (this.mExclusiveEntries.getCount() <= count && mExclusiveEntries.getSizeInBytes() <= size) {
return null;
}
ArrayList<Entry<K, V>> oldEntries = new ArrayList<>();
while (this.mExclusiveEntries.getCount() > count || mExclusiveEntries.getSizeInBytes() > size) {
@Nullable K key = mExclusiveEntries.getFirstKey();
if (key == null) {
if (mIgnoreSizeMismatch) {
mExclusiveEntries.resetSize();
break;
}
throw new IllegalStateException(
String.format(
"key is null, but exclusiveEntries count: %d, size: %d",
this.mExclusiveEntries.getCount(), mExclusiveEntries.getSizeInBytes()));
}
mExclusiveEntries.remove(key);
oldEntries.add(mCachedEntries.remove(key));
}
return oldEntries;
}

BitmapPrepareProducer

BitmapPrepareProducer只有一个作用,就是调用Bitmap#prepareToDraw()。prepareToDraw接口会构建一个用于绘制的缓存。从Android7.0开始,这个接口会在RenderThread线程上启动GPU的异步上传。开启硬件加速后,Bitmap必须上传到GPU才能被渲染。Bitmap第一次绘制时会默认就执行GPU上传动作,但是这个有几毫秒的耗时,图片越大耗时越高。通过调用prepareToDraw可以提前完成上传GPU的动作,这样可以节省第一帧的显示时间。


参考资料

Managing Bitmap Memory
Fresco内存机制(Ashmem匿名共享内存)
Android网络加载图片库对比:Fresco、Glide、Picasso
Android | Bitmap的Java对象GC之后,对应的native内存会回收吗?
Android图片缓存框架 - Fresco设置和清除缓存(十一)