Fresco 提炼总结

968 阅读11分钟

前言

开始本文之前,先简单聊一下阅读源码这件事,以前我没经验,看源码喜欢面面俱到,不放过任何一个细节,结果就是当时看明白了,之后很快就忘了,因为记的细节太多,关键点反而记不住。

现在学会了,看源码只看关键流程,而且,一般来说,设计良好的开源库的接口都设计得很好,把接口搞清楚了,代码架构也就基本清楚了,比如 Glide,最核心的接口其实就是 Request、DataFetcher、Target 三个而已,够清楚了吧?如果遗漏了什么关键的细节可以自己另外再针对性地去了解即可,这样逻辑更清晰,速度也更快。比如以前我看 Glide 的源码,花了一个多星期,现在看 Fresco,只需要一天。

所以,既然标题是“提炼总结”,那么本文就只讲关键点。

使用

Fresco 的官方文档其实已经很详细了,这里简单介绍一下:

如果需要自定义内存缓存、磁盘缓存等选项,可以参考下面的代码:

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 种:

  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