第 3 章 IMAGE PIPELINE 指南
最后更新于:2022-04-01 02:24:08
[TOC]
## Image Pipeline介绍
Image pipeline 负责完成加载图像,变成Android设备可呈现的形式所要做的每个事情。
大致流程如下:
1. 检查内存缓存,如有,返回
1. 后台线程开始后续工作
1. 检查是否在未解码内存缓存中。如有,解码,变换,返回,然后缓存到内存缓存中。
1. 检查是否在文件缓存中,如果有,变换,返回。缓存到未解码缓存和内存缓存中。
1. 从网络或者本地加载。加载完成后,解码,变换,返回。存到各个缓存中。
既然本身就是一个图片加载组件,那么一图胜千言。
![Image Pipeline Diagram](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-17_55d18d2697768.png)
**图片 3.1** Image Pipeline Diagram
上图中,`disk cache`实际包含了未解码的内存缓存在内,统一在一起只是为了逻辑稍微清楚一些。关于缓存,更多细节可以参考[这里](#)。
Image pipeline可以从[本地文件](#)加载文件,也可以从网络。支持PNG,GIF,WebP, JPEG。
### 各个Android系统的WebP适配
在3.0系统之前,Android是不支持WebP格式的。在4.1.2之前,扩展WebP格式是不支持的。在Image pipeline的支持下,从2.3之后,都可以使用WebP格式。
## 配置 Image Pipeline
对于大多数的应用,Fresco的初始化,只需要以下一句代码:
~~~
Fresco.initialize(context);
~~~
对于那些需要更多进一步配置的应用,我们提供了 ImagePipelineConfig。
以下是一个示例配置,列出了所有可配置的选项。几乎没有应用是需要以下这所有的配置的,列出来仅仅是为了作为参考。
~~~java
ImagePipelineConfig config = ImagePipelineConfig.newBuilder()
.setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier)
.setCacheKeyFactory(cacheKeyFactory)
.setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier)
.setExecutorSupplier(executorSupplier)
.setImageCacheStatsTracker(imageCacheStatsTracker)
.setMainDiskCacheConfig(mainDiskCacheConfig)
.setMemoryTrimmableRegistry(memoryTrimmableRegistry)
.setNetworkFetchProducer(networkFetchProducer)
.setPoolFactory(poolFactory)
.setProgressiveJpegConfig(progressiveJpegConfig)
.setRequestListeners(requestListeners)
.setSmallImageDiskCacheConfig(smallImageDiskCacheConfig)
.build();
Fresco.initialize(context, config);
~~~
请记得将配置好的`ImagePipelineConfig` 传递给 `Fresco.initialize!` 否则仍旧是默认配置。
#### 关于Supplier
许多配置的Builder都接受一个 Supplier 类型的参数而不是一个配置的实例。
创建时也许有一些麻烦,但这带来更多的利好:这允许在运行时改变创建行为。以内存缓存为例,每隔5分钟就可检查一下Supplier,根据实际情况返回不同类型。
如果你需要动态改变参数,那就是用Supplier每次都返回同一个对象。
~~~java
Supplier<X> xSupplier = new Supplier<X>() {
public X get() {
return new X(xparam1, xparam2...);
}
);
// when creating image pipeline
.setXSupplier(xSupplier);
~~~
#### 线程池
Image pipeline 默认有3个线程池:
1. 3个线程用于网络下载
1. 两个线程用于磁盘操作: 本地文件的读取,磁盘缓存操作。
1. 两个线程用于CPU相关的操作: 解码,转换,以及后处理等后台操作。
对于网络下载,你可以定制网络层的操作,具体参考:[自定义网络层加载](#).
对于其他操作,如果要改变他们的行为,传入一个 ExecutorSupplier 即可。
#### 内存缓存的配置
内存缓存和未解码的内存缓存的配置由一个Supplier控制,这个Supplier返回一个 MemoryCacheParams
#### 配置磁盘缓存
你可使用Builder模式创建一个 DiskCacheConfig:
~~~java
DiskCacheConfig diskCacheConfig = DiskCacheConfig.newBuilder()
.set....
.set....
.build()
// when building ImagePipelineConfig
.setMainDiskCacheConfig(diskCacheConfig)
~~~
#### 缓存统计
如果你想统计缓存的命中率,你可以实现 ImageCacheStatsTracker, 在这个类中,每个缓存时间都有回调通知,基于这些事件,可以实现缓存的计数和统计。
## 缓存
### 三级缓存
#### 1. Bitmap缓存
Bitmap缓存存储`Bitmap`对象,这些Bitmap对象可以立刻用来显示或者用于后处理
在5.0以下系统,Bitmap缓存位于ashmem,这样Bitmap对象的创建和释放将不会引发GC,更少的GC会使你的APP运行得更加流畅。
5.0及其以上系统,相比之下,内存管理有了很大改进,所以Bitmap缓存直接位于Java的heap上。
当应用在后台运行是,该内存会被清空。
#### 2. 未解码图片的内存缓存
这个缓存存储的是原始压缩格式的图片。从该缓存取到的图片在使用之前,需要先进行解码。
如果有调整大小,旋转,或者WebP编码转换工作需要完成,这些工作会在解码之前进行。
APP在后台时,这个缓存同样会被清空。
#### 3. 文件缓存
和未解码的内存缓存相似,文件缓存存储的是未解码的原始压缩格式的图片,在使用之前同样需要经过解码等处理。
和内存缓存不一样,APP在后台时,内容是不会被清空的。即使关机也不会。用户可以随时用系统的设置菜单中进行清空缓存操作。
### 用一个文件还是两个文件缓存?
如果要使用2个缓存,在[配置image pipeline](#) 时调用 `setMainDiskCacheConfig` 和 `setSmallImageDiskCacheConfig` 方法即可。
大部分的应用有一个文件缓存就够了,但是在一些情况下,你可能需要两个缓存。比如你也许想把小文件放在一个缓存中,大文件放在另外一个文件中,这样小文件就不会因大文件的频繁变动而被从缓存中移除。
至于什么是小文件,这个由应用来区分,在[创建image request](#), 设置 ImageType 即可:
~~~java
ImageRequest request = ImageRequest.newBuilderWithSourceUri(uri)
.setImageType(ImageType.SMALL)
~~~
如果你仅仅需要一个缓存,那么不调用`setSmallImageDiskCacheConfig`即可。Image pipeline 默认会使用同一个缓存,同时`ImageType`也会被忽略。
### 内存用量的缩减
在 [配置Image pipeline](#) 时,我们可以指定每个缓存最大的内存用量。但是有时我们可能会想缩小内存用量。比如应用中有其他数据需要占用内存,不得不把图片缓存清除或者减小或者我们想检查看看手机是否已经内存不够了。
Fresco的缓存实现了 DiskTrimmable 或者 MemoryTrimmable 接口。这两个接口负责从各自的缓存中移除内容。
在应用中,可以给 Image pipeline 配置上实现了 DiskTrimmableRegistry 和 MemoryTrimmableRegistry 接口的对象。
实现了这两个接口的对象保持着一个列表,列表中的各个元素在内存不够时,缩减各自的内存用量。
## 直接使用Image Pipeline
本页介绍Image pipeline的高级用法,大部分的应用使用[Drawees](#) 和image pipeline打交道就好了。
直接使用Image pipeline是较为有挑战的事情,这意味着要维护图片的内存使用。Drawees会根据各种情况确定图片是否需要在内存缓存中,在需要时加载,在不需要时移除。直接使用的话,你需要自己完成这些逻辑。
Image pipeline返回的是一个[CloseableReference](#)对象。在这些对象不需要时,Drawees会调用`.close()`方法。如果你的应用不使用Drawees,那你需要自己完成这个事情。
Java的GC机制会在Bitmap不使用时,清理掉Bitmap。但要GC时总是太迟了,另外GC是很昂贵的开销。GC大对象也会带来性能问题,尤其是在5.0以下系统。
### 调用 pipeline
首先[创建一个image request](#). 然后传递给 `ImagePipeline:`
~~~java
ImagePipeline imagePipeline = Fresco.getImagePipeline();
DataSource<CloseableReference<CloseableImage>>
dataSource = imagePipeline.fetchDecodedImage(imageRequest);
~~~
关于如果接收数据,请参考[数据源](#) 章节。
### 忽略解码
如果你不保持图片原始格式,不执行解码,使用`fetchEncodedImage`即可:
~~~java
DataSource<CloseableReference<PooledByteBuffer>>
dataSource = imagePipeline.fetchEncodedImage(imageRequest);
~~~
### 从Bitmap缓存中立刻取到结果
不像其他缓存,如果图片在内存缓存中有的话,可以在UI线程立刻拿到结果。
~~~java
DataSource<CloseableReference<CloseableImage>> dataSource =
mImagePipeline.fetchImageFromBitmapCache(imageRequest);
CloseableReference<CloseableImage> imageReference;
try {
imageReference = dataSource.getResult();
if (imageReference != null) {
CloseableImage image = imageReference.get();
// do something with the image
}
} finally {
dataSource.close();
CloseableReference.closeSafely(imageReference);
}
~~~
千万 **不要** 省略掉 `finally` 中的代码!
### 预加载图片
预加载图片可减少用户等待的时间,如果预加载的图片用户没有真正呈现给用户,那么就浪费了用户的流量,电量,内存等资源了。大多数应用,并不需要预加载。
Image pipeline 提供两种预加载方式。
预加载到文件缓存:
~~~java
imagePipeline.prefetchToDiskCache(imageRequest);
~~~
预加载到内存缓存:
~~~java
imagePipeline.prefetchToBitmapCache(imageRequest);
~~~
## 数据源和数据订阅者
数据源和 [Future](http://developer.android.com/reference/java/util/concurrent/Future.html), 有些相似,都是异步计算的结果。
不同点在于,数据源对于一个调用会返回一系列结果,Future只返回一个。
提交一个Image request之后,Imagepipeline返回一个数据源。从中获取数据需要使用数据订阅者(DataSubscriber).
### 当你仅仅需要Bitmap
如果你请求Image pipeline仅仅是为了获取一个 [Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html), 对象。你可以利用简单易用的 BaseBitmapDataSubscriber:
~~~java
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
// You can use the bitmap in only limited ways
// No need to do any cleanup.
}
@Override
public void onFailureImpl(DataSource dataSource) {
// No cleanup required here.
}
});
~~~
看起来很简单,对吧。下面是一些小警告:
千万 **不要** 把bitmap复制给`onNewResultImpl`函数范围之外的任何变量。订阅者执行完操作之后,imagepipeline会回收这个bitmap,释放内存。在这个函数范围内再次使用这个Bitmap对象进行绘制将会导致`IllegalStateException`。
### 通用的解决方案
如果你就是想维持对这个Bitmap对象的引用,你不能维持纯Bitmap对象的引用,可以利用[可关闭的引用(closeablereferences)](#) 和 BaseDataSubscriber:
~~~java
DataSubscriber dataSubscriber =
new BaseDataSubscriber<CloseableReference<CloseableImage>>() {
@Override
public void onNewResultImpl(
DataSource<CloseableReference<CloseableImage>> dataSource) {
if (!dataSource.isFinished()) {
FLog.v("Not yet finished - this is just another progressive scan.");
}
CloseableReference<CloseableImage> imageReference = dataSource.getResult();
if (imageReference != null) {
try {
CloseableImage image = imageReference.get();
// do something with the image
} finally {
imageReference.close();
}
}
}
@Override
public void onFailureImpl(DataSource dataSource) {
Throwable throwable = dataSource.getFailureCause();
// handle failure
}
};
dataSource.subscribe(dataSubscriber, executor);
~~~
这样,只要遵守[可关闭的引用使用规则](#),你就可以把这个`CloseableReference`复制给其他变量了。
## 可关闭的引用
**本页内容仅为高级使用作参考**
大部分的应用,直接使用[Drawees](#)就好了,不用考虑关闭的事情了。
Java带有垃圾收集功能,许多开发者习惯于不自觉地创建一大堆乱七八糟的对象,并且想当然地认为他们会从内存中想当然地消失。
在5.0系统之前,这样的做法对于操作Bitmap是极其糟糕的。Bitmap占用了大量的内存,大量的内存申请和释放引发频繁的GC,使得界面卡顿不已。
Bitmap 是Java中为数不多的能让Java开发者想念或者羡慕C++以及C++众多的指针库,比如[Boost](http://www.boost.org/doc/libs/1_57_0/libs/smart_ptr/smart_ptr.htm) 的东西。
Fresco的解决方案是: 可关闭的引用(CloseableReference)
为了正确地使用它,请按以下步骤进行操作:
### 1. 调用者拥有这个引用
我们创建一个引用,但我们传递给了一个调用者,调用者将持有这个引用。
~~~java
CloseableReference<Val> foo() {
Val val;
return CloseableReference.of(val);
}
~~~
### 2. 持有者在离开作用域之前,需要关闭引用
创建了一个引用,但是没有传递给其他调用者,在结束时,需要关闭。
~~~java
void gee() {
CloseableReference<Val> ref = foo();
try {
haa(ref);
} finally {
ref.close();
}
}
~~~
`finally` 中最适合做此类事情了。
### 3. 除了引用的持有者,闲杂人等**不得**关闭引用
作为一个参数传递,调用者持有这个引用,在下面的函数体中,不能关闭引用。
~~~java
void haa(CloseableReference<?> ref) {
Log.println("Haa: " + ref.get());
}
~~~
如果调用了 `.close()`, 调用者尝试调用 `.get()`时,会抛出`IllegalStateException`
### 4. 在赋值给变量前,先进行clone
在类中使用:
~~~java
class MyClass {
CloseableReference<Val> myValRef;
void mmm(CloseableReference<Val> ref) {
myValRef = ref.clone();
};
// caller can now safely close its copy as we made our own clone.
void close() {
CloseableReference.closeSafely(myValRef);
}
}
// MyClass的调用者需要关闭myValRef
~~~
在内部中使用:
~~~java
void haa(CloseableReference<?> ref) {
final CloseableReference<?> refClone = ref.clone();
executor.submit(new Runnable() {
public void run() {
try {
Log.println("Haa Async: " + refClone.get());
} finally {
refClone.close();
}
}
});
// 当前函数域内可安全关闭,闭包内为已经clone过的引用。
}
~~~