[TOC]
## Image Pipeline介紹
Image pipeline 負責完成加載圖像,變成Android設備可呈現的形式所要做的每個事情。
大致流程如下:
1. 檢查內存緩存,如有,返回
1. 后臺線程開始后續工作
1. 檢查是否在未解碼內存緩存中。如有,解碼,變換,返回,然后緩存到內存緩存中。
1. 檢查是否在文件緩存中,如果有,變換,返回。緩存到未解碼緩存和內存緩存中。
1. 從網絡或者本地加載。加載完成后,解碼,變換,返回。存到各個緩存中。
既然本身就是一個圖片加載組件,那么一圖勝千言。

**圖片 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過的引用。
}
~~~