Fresco 提煉總結

前言

開始本文以前,先簡單聊一下閱讀源碼這件事,之前我沒經驗,看源碼喜歡面面俱到,不放過任何一個細節,結果就是當時看明白了,以後很快就忘了,由於記的細節太多,關鍵點反而記不住。html

如今學會了,看源碼只看關鍵流程,並且,通常來講,設計良好的開源庫的接口都設計得很好,把接口搞清楚了,代碼架構也就基本清楚了,好比 Glide,最核心的接口其實就是 Request、DataFetcher、Target 三個而已,夠清楚了吧?若是遺漏了什麼關鍵的細節能夠本身另外再針對性地去了解便可,這樣邏輯更清晰,速度也更快。好比之前我看 Glide 的源碼,花了一個多星期,如今看 Fresco,只須要一天。android

因此,既然標題是「提煉總結」,那麼本文就只講關鍵點。git

使用

Fresco 的官方文檔其實已經很詳細了,這裏簡單介紹一下:github

若是須要自定義內存緩存、磁盤緩存等選項,能夠參考下面的代碼:算法

ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
    .setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier)    // Bitmap 緩存(內存緩存)
    .setDownsampleEnabled(true)                                       // 自動重採樣,若是不設置,默認爲 false
    .setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier)  // 未解碼的圖片的內存緩存
    .setExecutorSupplier(executorSupplier)                            // 線程池 
    .setMainDiskCacheConfig(mainDiskCacheConfig)                      // 磁盤緩存
    .setMemoryTrimmableRegistry(memoryTrimmableRegistry)              // 內存事件監聽
    .setProgressiveJpegConfig(progressiveJpegConfig)                  // 漸進式加載 JPEG 圖
    .setSmallImageDiskCacheConfig(smallImageDiskCacheConfig)          // 小文件磁盤緩存
    .build();
Fresco.initialize(context, config);
複製代碼

Fresco 中的線程池主要有 4 個(其實不止 4 個,但下面 4 個是比較關鍵的):緩存

  1. 用於網絡下載,線程數爲 3
  2. 用於磁盤操做(本地文件的讀取、磁盤緩存),線程數爲 2
  3. 用於解碼,線程數爲 CPU 個數
  4. 用於圖像變換、後期處理等操做,線程數爲 CPU 個數

緩存有 3 級:服務器

  1. Bitmap 緩存。在 5.0 如下,Bitmap 緩存位於 ashmem,這樣 Bitmap 對象的建立和釋放將不會引起 GC。5.0 及以上系統,內存管理有了很大改進,因此 Bitmap 直接存儲於 Java 堆中。
  2. 未解碼圖片的內存緩存。從該緩存取到的圖片在使用以前,須要先進行解碼。
  3. 磁盤緩存。存儲的是未解碼的原始壓縮格式的圖片,在使用以前一樣須要通過解碼等處理。若是須要將小文件獨立地放在一個緩存中,避免因大文件的頻繁變更而被從緩存中移除,能夠設置 setSmallImageDiskCacheConfig。是否爲小文件由應用區分,在建立 ImageRequest 時設置 setImageType 便可。

支持的圖片縮放方式有 3 種:markdown

  1. Scaling,一種畫布操做,一般是由硬件加速的。圖片實際大小保持不變,它只不過在繪製時被放大或縮小,默認啓用
  2. Resizing,一種軟件執行的管道操做,返回一張新的,尺寸不一樣的圖片。建立 ImageRequest 提供 ResizeOptions 便可使用,但只有 JPEG 能夠修改尺寸
  3. Downsampling,一樣是軟件實現的管道操做,它不會建立一張新的圖片,而是在解碼時改變圖片的大小。須要手動設置 setDownsample 爲 true,一樣須要提供 ResizeOptions

在 XML 中使用 SimpleDraweeView 並定義屬性是最簡單的用法:網絡

<com.facebook.drawee.view.SimpleDraweeView
  android:id="@+id/my_image_view"
  android:layout_width="20dp"
  android:layout_height="20dp"
  fresco:fadeDuration="300"
  fresco:actualImageScaleType="focusCrop"        // 縮放
  fresco:placeholderImage="@color/wait_color"    // 佔位圖
  fresco:placeholderImageScaleType="fitCenter"
  fresco:failureImage="@drawable/error"          // 加載失敗佔位圖
  fresco:failureImageScaleType="centerInside"
  fresco:retryImage="@drawable/retrying"         // 點擊從新加載的圖片
  fresco:retryImageScaleType="centerCrop"
  fresco:progressBarImage="@drawable/progress_bar" // 加載圖片時顯示的進度條
  fresco:progressBarImageScaleType="centerInside"
  fresco:progressBarAutoRotateInterval="1000"
  fresco:backgroundImage="@color/blue"             // 背景圖,最早繪製,不支持縮放
  fresco:overlayImage="@drawable/watermark"        // 疊加圖,最後繪製,不支持縮放
  fresco:pressedStateOverlayImage="@color/red"     // pressed 狀態下的疊加圖,不支持縮放
  fresco:roundAsCircle="false"                     // 圓形圖片
  fresco:roundedCornerRadius="1dp"                 // 圓角
/>
複製代碼

以後只須要設置 URI 便可:架構

Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/logo.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);
複製代碼

注意:

  1. Fresco 不支持相對路徑的 URI
  2. Drawees 不支持 wrap_content 屬性,只有在但願顯示固定的寬高比(fresco:viewAspectRatio)時,纔可使用 wrap_content
  3. SimpleDraweeView 繼承自 ImageView,但只有 fresco 提供的 actualImageScaleType 會起做用,系統的 scaleType 屬性無效
  4. 在 5.0 系統如下,Fresco 將 Bitmap 數據存在 ashmem 中,避開了 Java 堆內存。這要求圖片不使用時,要顯式地釋放內存。SimpleDraweeView 自動處理了這個釋放過程,因此若是沒有特殊狀況,儘可能使用 SimpleDraweeView。

代碼架構

Fresco 中的關鍵概念主要有 2 個:

  1. Drawees,負責圖片的呈現
  2. ImagePipeline,負責圖片的獲取和管理

其中,Drawees 由三部分組成:

  1. DraweeView,負責顯示圖片
  2. DraweeHierarchy,負責組織和維護最終顯示的 Drawable 對象,至關於圖層管理
  3. DraweeController,負責和 ImagePipeline 交互

ImagePipeline 負責加載圖像,大體流程以下:

  1. 檢查內存緩存,若有,返回
  2. 檢查是否在未解碼內存緩存中。若有,解碼,變換,返回,而後緩存到內存緩存中
  3. 檢查是否在磁盤緩存中,若是有,變換,返回。緩存到未解碼緩存和內存緩存中
  4. 從網絡或者本地加載。加載完成後,解碼,變換,返回。存到各個緩存中

其中,負責執行數據獲取工做的接口是 Producer:

public interface Producer<T> {
    void produceResults(Consumer<T> consumer, ProducerContext context);
}
複製代碼

負責將數據解碼爲 Bitmap、gif 等圖像類型的接口是 ImageDecoder:

public interface ImageDecoder {

    CloseableImage decode(
            @Nonnull EncodedImage encodedImage,
            int length,
            @Nonnull QualityInfo qualityInfo,
            @Nonnull ImageDecodeOptions options);
}
複製代碼

加載流程

構建 Producer 序列

在執行了 setImageURI 以後,Fresco 就會根據 Uri 選擇構建對應的 Producer 的序列:

private Producer getBasicDecodedImageSequence(ImageRequest imageRequest) {
    Uri uri = imageRequest.getSourceUri();

    switch (imageRequest.getSourceUriType()) {
        case SOURCE_TYPE_NETWORK:             // 網絡
            return getNetworkFetchSequence();
        case SOURCE_TYPE_LOCAL_IMAGE_FILE:    // 本地文件
            return getLocalImageFileFetchSequence();
        ...
    }
}
複製代碼

讀取 Bitmap 緩存

在具體執行 Producer 序列以前,Fresco 首先會檢查 Bitmap 內存緩存:

protected void submitRequest() {
    final T closeableImage = getCachedImage(); // 先從緩存中獲取
    if (closeableImage != null) {
        onNewResultInternal(...); // 獲取成功後回調
        return;
    }
    mDataSource = getDataSource(); // 不然構建數據源,從數據源中獲取
    final DataSubscriber<T> dataSubscriber = new BaseDataSubscriber<T>() { ... }; // 監聽加載過程
    mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor);
}

protected CloseableReference<CloseableImage> getCachedImage() {
    return mMemoryCache.get(mCacheKey);
}
複製代碼

這裏有一個問題,假如同一個 Bitmap 被引用了屢次,怎麼判斷何時釋放呢?對於這個問題,Fresco 是使用引用計數的方式來解決的:

public class SharedReference<T> {

    public void deleteReference() {
        if (decreaseRefCount() == 0) { // 若是計數爲 0,則釋放資源
            mResourceReleaser.release(mValue);
        }
    }
}
複製代碼

讀取未解碼的圖片緩存

若是沒法從 Bitmap 緩存中獲取到結果,Fresco 接着會執行一系列 Producer。首先會嘗試從未解碼的圖片緩存中獲取數據。實際負責此工做的生產者是 EncodedMemoryCacheProducer,通過一連串的調用,最終由 LruCountingMemoryCache 返回數據:

public class LruCountingMemoryCache<K, V> {

    @Nullable
    public CloseableReference<V> get(final K key) {
        synchronized (this) {
            // mExclusiveEntries 表明沒有被使用的 item,所以,經過 get 方法獲取到後,要從集合中移除掉它
            oldExclusive = mExclusiveEntries.remove(key);
            // mCachedEntries 包括全部被緩存的 item 
            Entry<K, V> entry = mCachedEntries.get(key);
            return newClientReference(entry);
        }
    }
}
複製代碼

根據 LRU 算法,在緩存大小超過限制時,就會移除最近最少使用的數據:

@Nullable
private synchronized ArrayList<Entry<K, V>> trimExclusivelyOwnedEntries(int count, int size) {
    while (mExclusiveEntries.getCount() > count || mExclusiveEntries.getSizeInBytes() > size) {
        K key = mExclusiveEntries.getFirstKey();
        // 正在使用中的 item 沒法被移除,所以只能從 mExclusiveEntries 中移除
        mExclusiveEntries.remove(key);
    }
}
複製代碼

讀取磁盤緩存

若是依然沒法從內存緩存中獲取,Fresco 就會嘗試到磁盤緩存中獲取數據(實際負責此工做的生產者是 DiskCacheProducer):

public class BufferedDiskCache {

    @Nullable
    private PooledByteBuffer readFromDiskCache(final CacheKey key) throws IOException {
    	// 獲取磁盤緩存
        final BinaryResource diskCacheResource = mFileCache.getResource(key);
        // 打開文件 I/O 流
        final InputStream is = diskCacheResource.openStream();
        // 從 I/O 流中讀取數據
        PooledByteBuffer byteBuffer = mPooledByteBufferFactory.newByteBuffer(is, (int) diskCacheResource.size());
        return byteBuffer;
    }
}
複製代碼

值得一提的是,雖然 Fresco 也是經過 LRU 算法來實現磁盤緩存的,但實現方式和 DiskLruCache 不一樣,DiskLruCache 經過日誌文件來判斷最近最少使用的文件,而 Fresco 則是在每次訪問緩存文件時更新其「LastModified」 屬性:

public class DefaultDiskStorage implements DiskStorage {

    public @Nullable BinaryResource getResource(String resourceId, Object debugInfo) {
    	// 獲取對應的文件
        final File file = getContentFileFor(resourceId);
        if (file.exists()) {
         	// 更新文件的 LastModified 屬性
            file.setLastModified(mClock.now());
            return FileBinaryResource.createOrNull(file);
        }
        return null;
    }
}
複製代碼

這樣,若是緩存文件大小超出了限制,就能夠經過這個值來排序,以刪除最近最少使用的文件:

public class DefaultEntryEvictionComparatorSupplier implements EntryEvictionComparatorSupplier {

    @Override
    public EntryEvictionComparator get() {
        return new EntryEvictionComparator() {
            @Override
            public int compare(DiskStorage.Entry e1, DiskStorage.Entry e2) {
                long time1 = e1.getTimestamp();
                long time2 = e2.getTimestamp();
                return time1 < time2 ? -1 : ((time2 == time1) ? 0 : 1);
            }
        };
    }
}
複製代碼

從網絡中讀取數據

若是本地緩存也沒有,那麼下一步就要到服務器中獲取數據了(實際負責此工做的是 NetworkFetchProducer):

public class HttpUrlConnectionNetworkFetcher {

    void fetchSync(HttpUrlConnectionNetworkFetchState fetchState, Callback callback) {
        HttpURLConnection connection = downloadFrom(fetchState.getUri(), MAX_REDIRECTS);
        InputStream is = connection.getInputStream();
        callback.onResponse(is, -1);
    }
}
複製代碼

解碼

成功獲取到數據以後,下一步就是經過解碼器將數據轉化爲 Bitmap、gif 等資源,這個工做是由 DecodeProducer 完成的,最終由 ImageDecoder 返回數據:

private final ImageDecoder mDefaultDecoder = new ImageDecoder() {
            @Override
            public CloseableImage decode(...) {
                ImageFormat imageFormat = encodedImage.getImageFormat();
                // 格式分別爲 JPEG、GIF、WEBP、Bitmap
                if (imageFormat == DefaultImageFormats.JPEG) {
                    return decodeJpeg(encodedImage, length, qualityInfo, options);
                } else if (imageFormat == DefaultImageFormats.GIF) {
                    return decodeGif(encodedImage, length, qualityInfo, options);
                } else if (imageFormat == DefaultImageFormats.WEBP_ANIMATED) {
                    return decodeAnimatedWebp(encodedImage, length, qualityInfo, options);
                }
                return decodeStaticImage(encodedImage, options);
            }
        };
複製代碼

解碼爲 Bitmap 時,默認的格式是 ARGB_8888:

public class ImageDecodeOptionsBuilder {
    private Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888;
}
複製代碼

根據系統版本的不一樣,Bitmap 的解碼方式主要分爲三種:

public static PlatformDecoder buildPlatformDecoder() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        return new ArtDecoder(...);
    } else if (gingerbreadDecoderEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // 默認爲 false
        return new GingerbreadPurgeableDecoder();
    } else {
        return new KitKatPurgeableDecoder();
    }
}
複製代碼

其中,API 大於等於 21 時,Bitmap 直接在 Java 堆中分配:

private CloseableReference<Bitmap> decodeFromStream(...) {
    Bitmap decodedBitmap = BitmapFactory.decodeStream(inputStream, null, options);
    return CloseableReference.of(decodedBitmap);
}
複製代碼

不然在使用 native 內存分配:

public abstract class DalvikPurgeableDecoder implements PlatformDecoder {
    public CloseableReference<Bitmap> decodeFromEncodedImageWithColorSpace(...) {
        Bitmap bitmap = decodeByteArrayAsPurgeable(bytesRef, options);
        return pinBitmap(bitmap);
    }

    public CloseableReference<Bitmap> pinBitmap(Bitmap bitmap) {
        // Real decoding happens here
        nativePinBitmap(bitmap);
        return CloseableReference.of(bitmap);
    }

    @DoNotStrip
    private static native void nativePinBitmap(Bitmap bitmap);
}
複製代碼

寫入數據到緩存

在解碼以前,Fresco 還會經過相關的消費者(一個 Producer 和一個 Consumer 相關聯)來將數據寫入到磁盤緩存、內存緩存中:

private static class DiskCacheWriteConsumer {

    @Override
    public void onNewResultImpl(EncodedImage newResult, @Status int status) {
        final ImageRequest imageRequest = mProducerContext.getImageRequest();
        final CacheKey cacheKey = mCacheKeyFactory.getEncodedCacheKey(...);
        mDefaultBufferedDiskCache.put(cacheKey, newResult);
    }
}
複製代碼
private static class EncodedMemoryCacheConsumer {

    @Override
    public void onNewResultImpl(EncodedImage newResult, @Status int status) {
        CloseableReference<PooledByteBuffer> ref = newResult.getByteBufferRef();
        mMemoryCache.cache(mRequestedCacheKey, ref);
    }
}
複製代碼

解碼完成以後,就將圖像緩存到 Bitmap 緩存中:

protected Consumer<CloseableReference<CloseableImage>> wrapConsumer() {
    return new DelegatingConsumer<>(consumer) {
        @Override
        public void onNewResultImpl(CloseableReference<CloseableImage> newResult) {
            mMemoryCache.cache(cacheKey, newResult);
        }
    };
}
複製代碼

顯示圖片

SimpleDraweeView 在設置 Uri 時就會與 DraweeHierarchy 裏的圖層綁定:

public class DraweeView<DH extends DraweeHierarchy> extends ImageView {

    public void setController(@Nullable DraweeController draweeController) {
        mDraweeHolder.setController(draweeController);
        super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
    }
}
複製代碼

這些圖層包括背景圖、佔位圖、實際加載的圖片、進度條、點擊重試佔位圖、加載失敗佔位圖、疊加圖等:

public class GenericDraweeHierarchy implements SettableDraweeHierarchy {

    private static final int BACKGROUND_IMAGE_INDEX = 0;
    private static final int PLACEHOLDER_IMAGE_INDEX = 1;
    private static final int ACTUAL_IMAGE_INDEX = 2;
    private static final int PROGRESS_BAR_IMAGE_INDEX = 3;
    private static final int RETRY_IMAGE_INDEX = 4;
    private static final int FAILURE_IMAGE_INDEX = 5;
    private static final int OVERLAY_IMAGES_INDEX = 6;

    GenericDraweeHierarchy(GenericDraweeHierarchyBuilder builder) {
        Drawable[] layers = new Drawable[numLayers];
        layers[BACKGROUND_IMAGE_INDEX] = buildBranch(builder.getBackground(), null);
        ... // 添加其它圖層

        mFadeDrawable = new FadeDrawable(layers, false, ACTUAL_IMAGE_INDEX);
        mTopLevelDrawable = new RootDrawable(mFadeDrawable);
        mTopLevelDrawable.mutate();
    }
}
複製代碼

當成功獲取到圖片以後,DraweeController 就會回調並通知 DraweeHierarchy 修改 Drawable:

public abstract class AbstractDraweeController {

    private void onNewResultInternal() {
        Drawable drawable = createDrawable(image);
        mSettableDraweeHierarchy.setImage(drawable, 1f, wasImmediate);
    }
}
複製代碼

Drawable 修改後就會刷新自身:

public class ForwardingDrawable extends Drawable {

    @Nullable
    public Drawable setCurrent(@Nullable Drawable newDelegate) {
        Drawable previousDelegate = setCurrentWithoutInvalidate(newDelegate);
        invalidateSelf();
        return previousDelegate;
    }
}
複製代碼

這樣 SimpleDraweeView 就能顯示最新的圖像了。

配置

Fresco 默認使用的 Bitmap 緩存大小根據 ActivityManager.getMemoryClass 來分配,通常是其返回值的 1/4:

private int getMaxCacheSize() {
    final int maxMemory = mActivityManager.getMemoryClass() * ByteConstants.MB;
    if (maxMemory < 32 * ByteConstants.MB) {
        return 4 * ByteConstants.MB;
    } else if (maxMemory < 64 * ByteConstants.MB) {
        return 6 * ByteConstants.MB;
    } else {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return 8 * ByteConstants.MB;
        } else {
            return maxMemory / 4;
        }
    }
}
複製代碼

默認使用的未解碼圖片的緩存大小則根據虛擬機最大可用的內存來分配,通常是 4MB:

private int getMaxCacheSize() {
    final int maxMemory = Runtime.getRuntime().maxMemory();
    if (maxMemory < 16 * ByteConstants.MB) {
        return 1 * ByteConstants.MB;
    } else if (maxMemory < 32 * ByteConstants.MB) {
        return 2 * ByteConstants.MB;
    } else {
        return 4 * ByteConstants.MB;
    }
}
複製代碼

默認使用的磁盤緩存大小默認爲 40MB,若是是磁盤可用容量很小的設備,則多是 10MB 或 2MB:

public class DiskCacheConfig {

    public static class Builder {
        private long mMaxCacheSize = 40 * ByteConstants.MB;
        private long mMaxCacheSizeOnLowDiskSpace = 10 * ByteConstants.MB;
        private long mMaxCacheSizeOnVeryLowDiskSpace = 2 * ByteConstants.MB;
    }
}
複製代碼

總結

Fresco 中負責顯示圖像的組件主要由三部分組成:

  1. DraweeView,負責顯示圖片
  2. DraweeHierarchy,負責組織和維護最終顯示的 Drawable 對象,至關於圖層管理,這些圖層包括背景圖、佔位圖、實際加載的圖片、進度條、點擊重試佔位圖、加載失敗佔位圖、疊加圖。
  3. DraweeController,負責和 ImagePipeline 交互,加載圖片成功後會刷新圖層

緩存有 3 級:

  1. Bitmap 緩存。在 5.0 如下,Bitmap 緩存位於 ashmem,這樣 Bitmap 對象的建立和釋放將不會引起 GC。這要求圖片不使用時,要顯式地釋放內存。SimpleDraweeView 自動處理了這個釋放過程,因此若是沒有特殊狀況,儘可能使用 SimpleDraweeView。5.0 及以上系統,內存管理有了很大改進,因此 Bitmap 直接存儲於 Java 堆中。

  2. 未解碼圖片的內存緩存。從該緩存取到的圖片在使用以前,須要先進行解碼。

  3. 磁盤緩存。存儲的是未解碼的原始壓縮格式的圖片,在使用以前一樣須要通過解碼等處理。若是須要將小文件獨立地放在一個緩存中,避免因大文件的頻繁變更而被從緩存中移除,能夠設置 setSmallImageDiskCacheConfig。是否爲小文件由應用區分,在建立 ImageRequest 時設置 setImageType 便可。

值得一提的是,雖然 Fresco 也是經過 LRU 算法來實現磁盤緩存的,但實現方式和 DiskLruCache 不一樣,DiskLruCache 經過日誌文件來判斷最近最少使用的文件,而 Fresco 則是經過文件的「LastModified」屬性來判斷。

線程池主要有 4 個:

  1. 用於網絡下載,線程數爲 3
  2. 用於磁盤操做(本地文件的讀取、磁盤緩存),線程數爲 2
  3. 用於解碼,線程數爲 CPU 個數
  4. 用於圖像變換、後期處理等操做,線程數爲 CPU 個數

ImagePipeline 負責加載圖像,大體流程以下:

  1. 檢查內存緩存,若有,返回
  2. 檢查是否在未解碼內存緩存中。若有,解碼,變換,返回,而後緩存到內存緩存中
  3. 檢查是否在磁盤緩存中,若是有,變換,返回。緩存到未解碼緩存和內存緩存中
  4. 從網絡或者本地加載。加載完成後,解碼,變換,返回。存到各個緩存中

其中:

  1. 負責執行數據獲取工做(包括從未解碼的圖片緩存、磁盤緩存、網絡中獲取)的接口是 Producer,Producer 以鏈的方式組織運行
  2. 在 Producer 獲取到數據以後負責對數據進行處理(好比緩存)的接口是 Consumer,一個 Consumer 對應一個 Producer
  3. 負責將數據解碼爲 Bitmap、Gif 等圖像類型的接口是 ImageDecoder,Bitmap 默認使用的格式是 ARGB_8888
相關文章
相關標籤/搜索