通过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 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
相关的类图结构:
ImageView DraweeView GenericDraweeHierarchy GenericDraweeView SimpleDraweeView DraweeHierarchy getTopLevelDrawable() : Drawable SettableDraweeHierarchy GenericDraweeHierarchy DraweeController DraweeHolder GenericDraweeHierarchy VisibilityCallback RootDrawable AbstractDraweeController PipelineDraweeController
图片加载过程 这个时序图是以文章开头的SimpleDraweeView
的示例代码为例进行绘制。
SimpleDraweeView SimpleDraweeView PipelineDraweeController DraweeHolder DraweeHolder RootDrawable RootDrawable ImagePipeline ImagePipeline DataSource setImageURI(); Framework.onViewCreated 中调用 构造 PipelineDraweeController onAttachedToWindow (父类方法) onAttach onAttach 此时还未设置visible,不会 调用attachController onVisibilityAggregated (父类ImageView的方法) setVisible onVisibilityChange attachController onAttach submitRequest getCachedImage 获取缓存Bitmap getDataSource fetchDecodedImage 生成生产者序列 submitFetchRequest() 构造时传入 生产者队列 DataSource 生产者队列 启动执行 返回实例 返回 DataSource<CloseableReference<CloseableImage>> subscribe(DataSubscriber)注册订阅者 返回图片Bitmap。父类的onNewResultInternal()会被执行 createDrawable hierarchy.setImage() 显示图片
上图中DateSource是一个CloseableProducerToDataSourceAdapter
实例,如下图类图所示:
DataSource CloseableReference<CloseableImage> Producer CloseableReference<CloseableImage> void produceResults(Consumer<T>, ProducerContext) AbstractDataSource CloseableReference<CloseableImage> AbstractProducerToDataSourceAdapter CloseableReference<CloseableImage> CloseableProducerToDataSourceAdapter CloseableImage DataSubscriber CloseableReference<CloseableImage> 依赖 构造函数中调用 1 n
Producer就是图片的生产者队列,会执行网络请求、缓存读写、图片解码等等图片处理任务。下文会详细介绍生产者队列。
PipelineDraweeController的构造过程 SimpleDraweeView SimpleDraweeView PipelineDraweeControllerBuilderSupplier PipelineDraweeControllerBuilderSupplier PipelineDraweeControllerBuilder PipelineDraweeController init get() PipelineDraweeControllerBuilder 返回 PipelineDraweeControllerBuilder setImageURI setUri() setOldController() 调用父类build() 调用父类 buildController() obtainController() new PipelineDraweeController 调用父类 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 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(); } }; } 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构造相关的类图:
PipelineDraweeControllerBuilderSupplier PipelineDraweeControllerBuilder get() 单例 ImagePipeline PipelineDraweeControllerBuilder PipelineDraweeControllerFactory AbstractDraweeControllerBuilder getDataSourceSupplierForRequest() Supplier DataSource<CloseableReference<CloseableImage>> 构造 构造内部类返回
三级缓存机制 Fresco采用了三级缓存机制,两级内存,一级磁盘。
Bitmap内存缓存 原始图片内存缓存 磁盘缓存 MemoryCache CacheKey, CloseableImage CountingMemoryCache CacheKey, CloseableImage LruCountingMemoryCache CacheKey, CloseableImage MemoryCache CacheKey, PooledByteBuffer CountingMemoryCache CacheKey, PooledByteBuffer LruCountingMemoryCache CacheKey, PooledByteBuffer BufferedDiskCache FileCache DiskStorageCache ImagePipeline mBitmapMemoryCache mEncodedMemoryCache mMainBufferedDiskCache mSmallImageBufferedDiskCache
内存缓存池 Fresco的第一级和第二级内存缓存都是采用LruCountingMemoryCache
来进行内存管理。
LruCountingMemoryCache CountingLruMap<K, Entry<K, V>> mExclusiveEntries; CountingLruMap<K, Entry<K, V>> mCachedEntries; CountingLruMap K, Entry<K, V> LinkedHashMap<K, V> mMap; int mSizeInBytes = 0; CountingLruMap K, Entry<K, V> LinkedHashMap<K, V> mMap; int mSizeInBytes = 0; LinkedHashMap K, V LinkedHashMap K, 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 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) { releaseClientReference(entry); } }); } 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(); } 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 ); 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(); ...... mExclusiveEntries.remove(key); oldEntries.add(mCachedEntries.remove(key)); } return oldEntries; }
内存块缓存池-MemoryChunkPool MemoryChunkPool BufferMemoryChunkPool BufferMemoryChunk AshmemMemoryChunkPool NativeMemoryChunkPool NativeMemoryChunk MemoryChunk AshmemMemoryChunk BasePool MemoryChunk SparseArray<Bucket<MemoryChunk>> mBuckets Bucket MemoryChunk Queue<MemoryChunk> mFreeList new new new 1 n 1 n
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=>value 1024 (1KB) 5 2048 (2KB) 5 4096 (4KB) 5 8192 (8KB) 5 16384 (16KB) 5 32768 (32KB) 5 65536 (64KB) 5 131072 (128KB) 5 262144 (256KB) 2 524288 (512KB) 2 1048576 (1MB) 2
当需要获取内存块时,会调用MemoryChunkPool#get(int size)
来获取一个MemoryChunk。比如要获取一个60KB大小的内存块,就会在mBucketSizes数组中找大于等于60KB的最小值,所以,最终内存块大小是64KB。申请内存块时,如果内存池中已经有对应大小的内存块就返回,否则会创建一个新的内存块,并申请内存。
Bucket<MemoryChunk>
中维护的都是相同大小的内存块,使用一个Queue维护空闲的内存块,当内存块被取走后,就在Queue中删除了。当内存块使用完后再放入Queue中。
使用场景:
NetworkFetchProducer中,图片从网络下载后,原始图片数据就会写入到一个MemoryChunk中。
字节内存缓存池 ByteArrayPool BasePool ByteArray Pool byte [] ByteArrayPool GenericByteArrayPool
字节缓存池与内存块缓存池的机制是一样的,区别是内存块缓存池使用了1KB、2KB、4KB等一系列大小的内存块,而字节内存缓存池只有16KB大小的一种缓存块。一般作为一个临时内存来使用,使用完成后立马放回缓存池,
使用场景:
网络请求数据从输入流中读数据到一个临时的buffer中,这个buffer就是从字节内存缓存池中拿到的;
图片缓存到磁盘中时,内存的输入流中先读取数据到一个buffer中,然后从buffer写入到磁盘的输出流,其中buffer也是从字节内存缓存池中拿到;
图片解码器 ImageDecoder DefaultImageDecoder ImageDecoder mAnimatedGifDecoder ImageDecoder mAnimatedWebPDecoder PlatformDecoder mPlatformDecoder ImageDecoder mDefaultDecoder 内部类,根据图片格式选择Decoder PlatformDecoder DefaultDecoder OreoDecoder ArtDecoder PlatformDecoderFactory GingerbreadPurgeableDecoder KitKatPurgeableDecoder DalvikPurgeableDecoder BitmapPool
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 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 { Bitmap bitmap = decodeJPEGByteArrayAsPurgeable(bytesRef, length, options); 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 ; options.inPreferredConfig = bitmapConfig; options.inPurgeable = true ; options.inInputShareable = true ; options.inSampleSize = sampleSize; options.inMutable = true ; 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 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; ...... 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) { if (!willScale) { decoder->setSkipWritingZeroes (outputAllocator == &javaAllocator); decoder->setAllocator (outputAllocator); } else if (javaBitmap != NULL ) { decoder->setAllocator (&scaleCheckingAllocator); } } ...... SkBitmap decodingBitmap; if (!decoder->decode (stream, &decodingBitmap, prefConfig, decodeMode)) { return nullObjectReturn ("decoder->decode returned false" ); } ...... if (mode == SkImageDecoder::kDecodeBounds_Mode) { return NULL ; } ...... SkPixelRef* pr; if (isPurgeable) { pr = installPixelRef (outputBitmap, stream, sampleSize, doDither); } else { pr = outputBitmap->pixelRef (); } if (pr == NULL ) { return nullObjectReturn ("Got null SkPixelRef" ); } ...... 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; if (bitmap->getSize () >= 32 * 1024 ) { pr = new SkImageRef_ashmem (bitmapInfo, stream, sampleSize); } else { 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 void * SkImageRef::onLockPixels (SkColorTable** ct) { if (NULL == fBitmap.getPixels ()) { (void )this ->prepareBitmap (SkImageDecoder::kDecodePixels_Mode); } if (ct) { *ct = fBitmap.getColorTable (); } return fBitmap.getPixels (); } bool SkImageRef::prepareBitmap (SkImageDecoder::Mode mode) { ...... SkImageDecoder* codec; if (fFactory) { codec = fFactory->newDecoder (fStream); } else { codec = SkImageDecoder::Factory (fStream); } if (codec) { SkAutoTDelete<SkImageDecoder> ad (codec) ; codec->setSampleSize (fSampleSize); codec->setDitherImage (fDoDither); 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 ; } } ...... } 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); } codec->setSkipWritingZeroes (true ); AshmemAllocator alloc (&fRec, this ->getURI()) ; codec->setAllocator (&alloc); bool success = this ->INHERITED::onDecode (codec, stream, bitmap, config, mode); codec->setAllocator (NULL ); if (success) { 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); } 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" ); } ...... } bool SkImageDecoder::allocPixelRef (SkBitmap* bitmap, SkColorTable* ctable) const { return bitmap->allocPixels (fAllocator, ctable); } bool SkBitmap::allocPixels (Allocator* allocator, SkColorTable* ctable) { HeapAllocator stdalloc; if (NULL == allocator) { allocator = &stdalloc; } 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); fd = ashmem_create_region (fName, size); ...... 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 : SkAshmemRec* fRec; const char * fName; };
Bitmap复用 通过android.graphics.BitmapFactory#decodeStream(...)
接口进行图片解码时可以通过设置BitmapFactory.Options#inBitmap
来复用旧的Bitmap实例,这样可以避免频繁申请内存。但是有如下限制条件:
被复用的Bitmap的isMutable属性必须为true;
Android4.4及以上版本,只要新图片需要的pixels内存小于等于inBitmap的即可;
Android4.4以下版本有额外的限制,图片必须是jpeg或png格式,而且pixels内存大小必须相等,且inSampleSize必须是1;而且新图片的inPreferredConfig属性将会被inBitmap覆盖。
解码时Bitmap的复用代码在DefaultDecoder#decodeFromStream
中。被复用的Bitmap保存在BitmapPool中。当一个Bitmap不再被使用后(引用计数为0)就会被放到BitmapPool中:
Pool Bitmap BitmapPool BasePool Bitmap BucketsBitmapPool DummyBitmapPool LruBitmapPool PoolFactory BitmapPool getBitmapPool() Bucket Bitmap Bitmap 1 n 1 n
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 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放到缓存池中备用。
CloseableReference Bitmap void close() SharedReference Bitmap int mRefCount //引用计数 deleteReference() ResourceReleaser Bitmap void release(Bitmap v) BitmapPool void release(Bitmap v) Bitmap 调用 当引用计数为0时调用 release把Bitmap 释放到缓存池中
什么时候引用计数加一呢? 调用CloseableReference
子类clone方法克隆一个新的CloseableReference时,就等价于增加了一个使用者,应用计数就会加一。 新和旧的CloseableReference持有同一个SharedReference实例。
BitmapPool 不会无限扩张,最大支持多少个Bitmap?是否有清理策略?
SharedReference Bitmap Bitmap mValue int mRefCount ResourceReleaser<Bitmap> mResourceReleaser Bitmap CloseableReference Bitmap DefaultCloseableReference Bitmap CloseableImage CloseableBitmap CloseableStaticBitmap BaseCloseableImage BaseCloseableStaticBitmap DefaultCloseableStaticBitmap
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 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) { pool.release(ByteBuffer.allocate(DecodeBufferHelper.getRecommendedDecodeBufferSize())) } return pool }
图片处理生产者序列 Fresco从一个图片的URL下载图片并解码成Bitmap需要经过一个生产者序列的处理。在ImagePipeline#fetchDecodedImage
中开始生成队列并执行任务序列。下图展示了图片处理的所有生产者,其中有些序列默认是不使用的,除非进行特别配置或者特定场景。图中箭头的方向是图片数据流动的方向,而调用顺序正好相反,后面的Producer持有前面的Producer,当然这个持有只是对Producer接口的依赖,并不是直接依赖实现类。
图片数据 NetworkFetchProducer WebpTranscodeProducer PartialDiskCacheProducer DiskCacheWriteProducer DiskCacheReadProducer EncodedMemoryCacheProducer EncodedProbeProducer EncodedCacheKeyMultiplexProducer AddImageTransformMetaDataProducer ResizeAndRotateProducer DecodeProducer BitmapMemoryCacheProducer BitmapMemoryCacheKeyMultiplexProducer ThreadHandoffProducer BitmapMemoryCacheGetProducer BitmapProbeProducer PostprocessorProducer PostprocessedBitmapMemoryCacheProducer BitmapPrepareProducer DelayProducer Bitmap
NetworkFetchProducer Fresco默认使用HttpURLConnection
而不是Okhttp来请求网络图片,在生成ImagePipelineConfig时需要使用OkHttpImagePipelineConfigFactory
来使用Okhttp来请求网络。
Producer EncodedImage NetworkFetchProducer NetworkFetcher MemoryChunkPool ByteArrayPool BaseNetworkFetcher HttpUrlConnectionNetworkFetcher OkHttpNetworkFetcher MemoryPooledByteBufferFactory newOutputStream() MemoryPooledByteBufferOutputStream MemoryPooledByteBuffer toByteBuffer() MemoryPooledByteBuffer MemoryChunk return return toByteBuffer()
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 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; while ((length = responseData.read(ioArray)) >= 0 ) { if (length > 0 ) { 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中。
PooledByteBuffer CloseableReference PooledByteBuffer EncodedImage MemoryPooledByteBuffer close() DefaultCloseableReference PooledByteBuffer SharedReference PooledByteBuffer int mRefCount ResourceReleaser PooledByteBuffer Closeable CloseableReference MemoryChunk MemoryChunkPool ResourceReleaser MemoryChunk release() MemoryChunk
DiskCacheWriteProducer DiskCacheWriteProducer用于缓存原始图片数据,包括两级缓存:内存和磁盘。
DiskCacheWriteProducer mDefaultBufferedDiskCache mSmallImageBufferedDiskCache BufferedDiskCache StagingArea Map<CacheKey, EncodedImage> mMap FileCache BinaryResource getResource(CacheKey key) BinaryResource insert(CacheKey key, WriterCallback writer) DiskStorageCache DiskStorage DynamicDefaultDiskStorage
DiskCacheWriteProducer接收到下级Producer的EncodedImage后就会调用BufferedDiskCache#put()
进行缓存。BufferedDiskCache在缓存图片时,会先把图片(EncodedImage)保存到StagingArea(mMap)中,即内存缓存,下次获取时也会优先从StagingArea中获取内存中的图片缓存。然后,在子线程中写入磁盘缓存。
DiskCacheReadProducer DiskCacheReadProducer mDefaultBufferedDiskCache mSmallImageBufferedDiskCache BufferedDiskCache
DiskCacheReadProducer也持有两个BufferedDiskCache,与DiskCacheWriteProducer中的同一个对象实例。从DiskCacheWriteProducer的分析中我们知道图片会优先保存到内存中(StagingArea),读取时也优先使用内存中的缓存。
DiskCacheReadProducer启动后会先从缓存读取图片,如果存在缓存的图片就不会再调用下一级Producer,直接把结果返回给上一级。如果没有读取到缓存的图片,就会继续调用下一级Producer。
EncodedMemoryCacheProducer MemoryCache CacheKey, PooledByteBuffer Producer EncodedImage EncodedMemoryCacheProducer 下一级生产者 CountingMemoryCache CacheKey, PooledByteBuffer LruCountingMemoryCache CacheKey, PooledByteBuffer CountingMemoryCache.Entry CacheKey, PooledByteBuffer CountingLruMap CacheKey, Entry n 1
EncodedMemoryCacheProducer执行时,会首先从MemoryCache读取缓存在内存中的图片,如果有缓存,则将缓存的图片传递给上一级生产者,不再执行下一级生产者。
根据CacheKey从缓存中获取图片 执行下一级生产者 收到下级生产者返回的图片 缓存图片 不存在 缓存图片存在? 存在 返回图片给上一级生产者
加入缓存或者获取缓存等操作,都会触发缓存的清理,清理时如果超过了最大数量或者最大存储空间,都会进行清理,被清理掉的都是不被使用的图片,而且时最老的优先被清理。
DecodeProducer Producer CloseableReference<CloseableImage> DecodeProducer ByteArrayPool ImageDecoder Executor
BitmapMemoryCacheProducer MemoryCache CacheKey, CloseableImage CountingMemoryCache CacheKey, CloseableImage LruCountingMemoryCache CacheKey, CloseableImage BitmapMemoryCacheProducer MemoryTrimmable void 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 public MemoryCacheParams get () { return new MemoryCacheParams ( getMaxCacheSize(), MAX_CACHE_ENTRIES, MAX_EVICTION_QUEUE_SIZE, MAX_EVICTION_QUEUE_ENTRIES, MAX_CACHE_ENTRY_SIZE, PARAMS_CHECK_INTERVAL_MS); } private int getMaxCacheSize () { 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 ; } }
从缓存获取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 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); if (entry != null ) { clientRef = newClientReference(entry); } } maybeNotifyExclusiveEntryRemoval(oldExclusive); maybeUpdateCacheParams(); maybeEvictEntries(); return clientRef; } 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(); } 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 ); 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设置和清除缓存(十一)