Fresco圖片框架內部實現原理探索

目前流行的網絡圖片框架:
Picasso、Universal Image Loader、Volley的(ImageLoader、NetworkImageView)、Glide和Fresco

簡明的介紹下(具體細節和功能可看源碼和wiki):
其中Picasso和Universal Image Loader相比其它的算是最輕量級的圖片框架了,它們擁有較少的方法數,Universal Image Loader是這五個框架中定製性最強的,它內部實現還是按網絡框架的套路走:HttpUrlConnection+線程池+Handler,支持漸顯效果。
而Picasso只有一些圖片加載框架應有的基本功能,所以因此它是最輕量的,在需求只要基本的圖片加載與雙緩存功能下,可以選Picasso作爲項目的基礎庫,Picasso它內部默認是使用OkHttpClient作爲加載網絡圖片的下載器,畢竟不用自家用誰的,在OkHttpClient沒有的情況下則使用HttpUrlConnection,同上面一樣,下載器+線程池+Handler,不過它內部的線程池比較有意思,線程池的線程數量是根據當前的網絡環境來動態改變的,wifi網絡下爲4,4G爲3,3G爲2,2G爲1,其它情況下默認爲3,支持漸顯效果。
Volley的沒什麼可說的,基本功能都有,網絡框架的附贈功能。

Glide的話,Google官方推薦,支持Gif、圖片縮略圖、本地視頻解碼、請求和動畫生命週期的自動管理、漸顯動畫、支持OkHttp和Volley等等,默認是使用HttpUrlConnection加載圖片的,源碼灰常多,200多個類,不想看

Fresco我認爲是這幾個框架中性能最佳的一個框架,着重介紹,它內部用了大量的建造者模式、單例模式、靜態工廠模式、生產/消費者模式。內部實現比較複雜,就拿圖片加載來說,是通過在異步線程中回調圖片的輸入流,然後通過一系列讀取、寫入、轉化成EncodedImage,然後再Decode成Bitmap,通過Handler轉給UI線程顯示,通過IO操作存儲在硬盤緩存目錄下。

Fresco性能上的優點

優一:

1、支持webp格式的圖片,是Google官方推行的,它的大小比其它格式圖片的大小要小一半左右,目前各個大公司都漸入的使用這種圖片格式了,比如:Youtube、Gmail、淘寶、QQ空間等都已嚐鮮,使用該格式最大的優點就是輕量、省流量、圖片加載迅速。而Fresco是通過jni來實現支持WebP格式圖片。

優二:

2、5.0以下系統:使用”ashmem”(匿名共享內存)區域存儲Bitmap緩存,這樣Bitmap對象的創建、釋放將永遠不會觸發GC,關於”ashmem”存儲區域,它是一個不在Java堆區的一片存儲內存空間,它的管理由Linux內核驅動管理,不必深究,只要知道這塊存儲區域是別於堆內存之外的一塊空間就行了,且這塊空間是可以多進程共享的,GC的活動不會影響到它。5.0以上系統,由於內存管理的優化,所以對於5.0以上的系統Fresco將Bitmap緩存直接放到了堆內存中。

關於”ashmem”的存儲區域,我們的應用程序並不能像訪問堆內存一樣直接訪問這塊內存塊,但是也有一些例外,對於Bitmap而言,有一種爲”Purgeable Bitmap”可擦除的Bitmap位圖是存儲在這塊內存區域中的,BitmapFactory.Options中有這麼一個屬性inPurgeable

?
1
2
3
<code class="hljs avrasm">BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);</code>

所以通過配置inPurgeable = true這個屬性,這樣解碼出來的Bitmap位圖就存儲在”ashmem”區域中,之後用到”ashmem”中得圖片時,則把這個圖片從這個區域中取出來,渲染完畢後則放回這個位置。

既然Fresco中Bitmap緩存在5.0以前是放在”ashmem”中,GC並不會回收它們,且也不會被”ashmeme”內置的清除機制回收它們,所以這樣雖然使得在堆中不會造成內存泄露,而在這塊區域可能造成內存泄露,Fresco中採取的辦法則是使用引用計數的方式,其中有一個SharedReference這個類,這個類中有這麼兩個方法:addReference()和deleteReference(),通過這兩個基本方法來對引用進行計數,一旦計數爲零時,則對應的資源將會清除(如:Bitmap.recycle()等),而Fresco爲了考慮更容易被我們使用,又提供了一個CloseableReference類,該類可以說是SharedReference類上功能的封裝,CloseableReference同時也實現了Cloneable、Closeable接口,它在調用.clone()方法時同時會調用addReference()來增加一個引用計數,在調用.close()方法時同時會調用deleteReference()來刪除一個引用計數,所以在使用Fresco的使用,我們都是與CloseableReference類打交道,使用CloseableReference必須遵循以下兩條規則:
1、在賦值CloseableReference給新對象的時候,調用.clone()進行賦值
2、在超出作用域範圍的時候,必須調用.close(),通常會在finally代碼塊中調用

?
1
2
3
4
5
6
7
8
<code class="hljs cs">void gee() {
  CloseableReference<val> ref = foo();
  try {
    haa(ref);
  } finally {
    ref.close();
  }
}</val></code>

遵循這些規則可以有效地防止內存泄漏。

優三:

3、使用了三級緩存:Bitmap緩存+未解碼圖片緩存+硬盤緩存。
其中前兩個就是內存緩存,Bitmap緩存根據系統版本不同放在了不同內存區域中,而未解碼圖片的緩存只在堆內存中,Fresco分了兩步做內存緩存,這樣做有什麼好處呢?第一個好處就如上的第二條,第二個好處是加快圖片的加載速度,Fresco的加載圖片的流程爲:查找Bitmap緩存中是否存在,存在則直接返回Bitmap直接使用,不存在則查找未解碼圖片的緩存,如果存在則進行Decode成Bitmap然後直接使用並加入Bitmap緩存中,如果未解碼圖片緩存中查找不到,則進行硬盤緩存的檢查,如有,則進行IO、轉化、解碼等一系列操作,最後成Bitmap供我們直接使用,並把未解碼(Encode)的圖片加入未解碼圖片緩存,把Bitmap加入Bitmap緩存中,如硬盤緩存中沒有,則進行Network操作下載圖片,然後加入到各個緩存中。

既然Fresco使用了三級緩存,而有兩級是內存緩存,所以當我們的App在後臺時或者在內存低的情況下在onLowMemory()方法中,我們應該手動清除應用的內存緩存,我們可以使用下面的方式:

?
1
2
3
4
5
6
7
8
<code class="hljs avrasm">        ImagePipeline imagePipeline = Fresco.getImagePipeline();
        //清空內存緩存(包括Bitmap緩存和未解碼圖片的緩存)
        imagePipeline.clearMemoryCaches();
        //清空硬盤緩存,一般在設置界面供用戶手動清理
        imagePipeline.clearDiskCaches();
 
        //同時清理內存緩存和硬盤緩存
        imagePipeline.clearCaches();</code>

優四:

4、Fresco框架的ImagePipeline設計圖
這裏寫圖片描述
從設計圖中可以看出,UIThread只做圖片的顯示和從內存緩存中加載圖片這兩件事,而其它事情如:圖片的Decode、內存緩存的寫、硬盤緩存的IO操作、網絡操作等都用非UIThread來處理了,這使得UIThread專注界面的顯示,而其它工作由其它線程完成,使UI更加流暢。

<h2 id="fresco中的mvc模式">Fresco中的MVC模式

Fresco框架整體是一個MVC模式

DraweeView——View
DraweeController——Control
DraweeHierarchy——Model

它們之間的關係大致如下:
DraweeHierarchy意爲視圖的層次結構,用來存儲和描述圖片的信息,同時也封裝了一些圖片的顯示和視圖層級的方法。
DraweeView用來顯示頂層視圖(getTopLevelDrawable())。DraweeController控制加載圖片的配置、頂層顯示哪個視圖以及控制事件的分發。
【注】DraweeView目前版本時繼承於ImageView,但這並不意味着我們可以隨意的使用ImageView相關的方法(如:setScaleType等),官方並不建議我們使用,因爲後期DraweeView將繼承於View,所以最好只使用DraweeView控件內置的方法。


DraweeHierarchy

DraweeHierarchy除了描述了視圖的信息和存儲6種視圖外,其中還對我們提供了一些額外的方法,比如:讓圖片漸漸顯示Fade效果、設置默認狀態下顯示的圖片、設置點擊時顯示的圖片和加載失敗時顯示的圖片等方法,這些方法可以在我們加載其它圖片時保持一些良好的交互效果,值得注意的是,DraweeHierarchy是一個接口,只提供了一個默認方法:

?
1
2
3
4
5
6
7
8
9
<code class="hljs java">public interface DraweeHierarchy {
 
  /**
   * Returns the top level drawable in the corresponding hierarchy. Hierarchy should always have
   * the same instance of its top level drawable.
   * @return top level drawable
   */
  Drawable getTopLevelDrawable();
}</code>

這個方法從官方註釋上來看,是得到當前視圖中最頂層的那個Drawable,如:
下面這個視圖層次最頂層的視圖爲FadeDrawable

?
1
2
3
4
5
6
7
8
9
<code class="hljs 1c">   o FadeDrawable (top level drawable)
   |
   +--o ScaleTypeDrawable
   |  |
   |  +--o BitmapDrawable
   |
   +--o ScaleTypeDrawable
      |
      +--o BitmapDrawable</code>

所以,在Fresco框架中,並不是一個DraweeView只能設置一個圖片(Drawable),而是可以設置一個視圖圖層(類似Android中的LayerDrawable可以設置視圖疊加),然後通過DraweeHolder在不同狀態下得到(getTopLevelDrawable())最頂層那個圖片從而使得DraweeView顯示不同的視圖,比如下面這個按下時顯示一個overlay(頂層)圖片效果:
這裏寫圖片描述
我想這也是Facebook不想把DraweeView這個組件單純的定義爲一個的ImageView的原因吧。

DraweeView

DraweeView是官方給我們提供顯示圖片的一個基類,我們在使用過程中大多時候並不需要用到它,而是用到一個官方已經簡單封好的SimpleDraweeView類,DraweeView類中提供了與DraweeController和DraweeHierarchy交互的接口,而與它們之間的交互本質上是通過一個DraweeHolder類進行交互,這類DraweeHolder是協調DraweeView、DraweeHierarchy、DraweeController這三個類交互工作的核心類,像平時我們都會這樣使用:

比如:配置一個DraweeHierarchy簡便起見通常會使用SimpleDraweeView直接設置:

?
1
2
3
4
5
6
7
<code class="hljs avrasm">SimpleDraweeView simpleDraweeView = (SimpleDraweeView) findViewById(R.id.drawee_view);
        simpleDraweeView.setImageURI(uri);
        //創建一個DraweeHierarchy
        GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources()).setPlaceholderImage(getDrawable(R.drawable.holder)).build();
        //設置一個hierarchy
simpleDraweeView.setHierarchy(hierarchy);</code>

其實上述方法最終都是通過mController.setHierarchy(hierarchy);來設置的,本質是調用DraweeHolder類封裝好的setHierarchy()方法,可以看看其內部:
DraweeHolder::setHierarchy()

?
1
2
3
4
5
6
<code class="hljs r"public void setHierarchy(DH hierarchy) {
    //...
    if (mController != null) {
      mController.setHierarchy(hierarchy);
    }
  }</code>

所以通過simpleDraweeView.setHierarchy(hierarchy);來設置等價於通過構建一個DraweeController來設置,如下:

?
1
2
3
4
<code class="hljs avrasm">        DraweeController controller = Fresco.newDraweeControllerBuilder().setUri(url).setXxx()...build();
        //通過DraweeController設置
        controller.setHierarchy();
        simpleDraweeView.setController(controller);</code>

這個DraweeHolder類的出現,更準確的定位是降低耦合度、解耦的定位。

對於上面我們知道了通過simpleDraweeView.setHierarchy(hierarchy);來設置等價於通過DraweeController::setHierarchy()來設置,那麼simpleDraweeView.setController(controller);有沒有等價的呢?答案是沒有,因爲要加載圖片那麼就必須設置一個DraweeController來控制圖片的加載,(當然我們如果設置了SimpleDraweeView的一些屬性,那麼默認也會創建一個DraweeHierarchy),而我們平時簡便的寫法:

?
1
2
3
<code class="hljs avrasm">SimpleDraweeView simpleDraweeView = (SimpleDraweeView) findViewById(R.id.drawee_view);
        simpleDraweeView.setImageURI(uri);</code>

這是一段最基本的寫法,我們都知道這樣設置了Fresco內部就自動會給我們加載圖片了,而網上也流傳着另外一種加載圖片的方法,爲:

?
1
2
<code class="hljs avrasm">        DraweeController controller = Fresco.newDraweeControllerBuilder().setUri(url).build();
        simpleDraweeView.setController(controller);</code>

其實這兩種寫法都是一種寫法,Fresco真正加載圖片僅僅只有這一種方法,就是通過simpleDraweeView.setController(controller);來設置,只不過我們可以對DraweeController和DraweeHierarchy做各種各樣的配置來達到我們想要的效果,我們可以看看simpleDraweeView.setImageURI(uri);的源碼,其實還是通過setController(controller);設置一個控制器來控制圖片的加載,源碼爲:

?
1
2
3
4
5
6
7
8
<code class="hljs bash"public void setImageURI(Uri uri, @Nullable Object callerContext) {
    DraweeController controller = mSimpleDraweeControllerBuilder
        .setCallerContext(callerContext)
        .setUri(uri)
        .setOldController(getController())
        .build();
    setController(controller);
  }</code>

所以,使用Fresco通用的寫法便是:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
<code class="hljs avrasm">        SimpleDraweeView simpleDraweeView = (SimpleDraweeView) findViewById(R.id.drawee_view);
 
        GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources())
                .setFadeDuration(400)
                .setPlaceholderImage(getDrawable(R.drawable.holder))
                .setFailureImage(getDrawable(R.drawable.fail))
                .build();
 
        DraweeController controller = Fresco.newDraweeControllerBuilder().setUri(uri).setOldController(simpleDraweeView.getController()).build();
        controller.setHierarchy(hierarchy);
 
        simpleDraweeView.setController(controller);</code>

或者直接使用ImageRequet:

?
1
2
3
<code class="hljs avrasm">    ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)).build();
    DraweeController draweeController = Fresco.newDraweeControllerBuilder().setImageRequest(imageRequest).setOldController(simpleDraweeView.getController()).build();
    simpleDraweeView.setController(controller);</code>

當然配置其它屬性只要設置Controller和Hierarchy相應的方法即可。
】:上面中用到了一個這個方法simpleDraweeView.getController();當然也還有simpleDraweeView.getHierarchy();,這兩個方法是返回當前DraweeView所設置的Control和Hierarchy,使用這兩個方法的好處是複用以前創建的Control和Hierarchy對象,因爲重新創建一個對象肯定不如複用好,而且創建相對耗時,所以官方也建議我們複用這兩個對象。如果你用simpleDraweeView.getHierarchy()來加載圖片,那麼它將不可能爲空,除非你什麼不設置,而getController()則可能爲空,所以在使用工廠方法

?
1
<code class="hljs avrasm">Fresco.newDraweeControllerBuilder().setUri(uri).setOldController(simpleDraweeView.getController()).build();</code>

來創建一個DraweeController的時候給它配置一個setOldController(),如果這裏面這個參數爲null,那麼就會重新創建一個DraweeController,如果不爲空則複用當前傳入的。


順便說一句,直接使用SimpleDraweeView就足夠了,畢竟配置功能都落在DraweeController和DraweeHierarchy身上,SimpleDraweeView僅僅起個顯示最頂層視圖的作用。

DraweeController

關於DraweeController,好像在上面已經講的差不多了,它主要就是起個控制圖片的加載和配置以及決定頂層顯示哪個視圖的作用,其它的它也可以設置設置個ControllerListener來監聽圖片加載的進度,也可以配置一個ImageRequest來設置漸進式JPEG圖片的加載,具體使用可以看其官方文檔。

認識Fresco中的視圖層次

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<code class="hljs 1c"> *     o FadeDrawable (top level drawable)
 *     |
 *     +--o ScaleTypeDrawable
 *     |  |
 *     |  +--o Drawable (placeholder image)
 *     |
 *     +--o ScaleTypeDrawable
 *     |  |
 *     |  +--o SettableDrawable
 *     |     |
 *     |     +--o Drawable (actual image)
 *     |
 *     +--o ScaleTypeDrawable
 *     |  |
 *     |  +--o Drawable (retry image)
 *     |
 *     +--o ScaleTypeDrawable
 *        |
 *        +--o Drawable (failure image)</code>

Fresco中對於DraweeHierarchy視圖層次的描述中,視圖層次的結構爲6種不同的視圖,依次爲頂層Drawable(可用FadeDrawable配置顯示效果)、默認顯示的Drawable、實際的Drawable(其中有一層可設置的Drawable包裹它作爲對我們實際加載到的Drawable進行配置)、重試的Drawable、加載失敗的Drawable。

上面這種層次從GenericDraweeHierarchy源碼中就有所體現,如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
<code class="hljs java"private Drawable mEmptyPlaceholderDrawable;
  private final Drawable mEmptyActualImageDrawable = new ColorDrawable(Color.TRANSPARENT);
  private final Drawable mEmptyControllerOverlayDrawable = new ColorDrawable(Color.TRANSPARENT);
  private final RootDrawable mTopLevelDrawable;
  private final FadeDrawable mFadeDrawable;
  private final SettableDrawable mActualImageSettableDrawable;
 
  private final int mPlaceholderImageIndex;
  private final int mProgressBarImageIndex;
  private final int mActualImageIndex;
  private final int mRetryImageIndex;
  private final int mFailureImageIndex;
  private final int mControllerOverlayIndex;</code>

上述定義了不同功能的Drawable,比如FadeDrawable和SettableDrawable都是可以對視圖進行配置。而也定義了6個int下標,這6個int類型的下標主要就是用於存儲視圖層次中各個Drawable中的位置,都是用一個Drawable[] layers ;數組進行存儲的:

?
1
2
3
4
5
6
<code class="hljs fix">    layers[mPlaceholderImageIndex] = placeholderImageBranch;
    layers[mActualImageIndex] = actualImageBranch;
    layers[mProgressBarImageIndex] = progressBarImageBranch;
    layers[mRetryImageIndex] = retryImageBranch;
    layers[mFailureImageIndex] = failureImageBranch;
    layers[mControllerOverlayIndex] = mEmptyControllerOverlayDrawable;</code>

這些存儲着視圖層次資源的layers數組將會由ArrayDrawable類進行管理和繪製,最終將需要顯示的視圖回調給RootDrawable::mTopLevelDrawable。

ImagePipelineConfig

我們對Fresco進行初始化時,有兩種方式:

?
1
2
3
4
<code class="hljs avrasm">        Fresco.initialize(this);
        //or
        ImagePipelineConfig pipelineConfig = ImagePipelineConfig.newBuilder(getApplicationContext()).build();
        Fresco.initialize(this,pipelineConfig);</code>

對於不對ImagePipeline進行配置的話,Fresco將採用默認的配置,而默認的配置到底配置了哪些信息這個我們要清楚。
對於ImagePipelineConfig我們主要可以配置以下屬性:

1、Bitmap.Config mBitmapConfig ——所加載圖片的配置,默認爲Bitmap.Config.ARGB_8888

2、Supplier<MemoryCacheParams> mBitmapMemoryCacheParamsSupplier——已解碼圖片的內存緩存,默認配置:緩存容量最大存儲256個Bitmap元素,緩存大小是根據最大可用內存來動態改變的,如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<code class="hljs java"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 {
      // We don't want to use more ashmem on Gingerbread for now, since it doesn't respond well to
      // native memory pressure (doesn't throw exceptions, crashes app, crashes phone)
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
        return 8 * ByteConstants.MB;
      } else {
        return maxMemory / 4;
      }
    }
  }</code>

3、mDecodeMemoryFileEnabled——是否根據不同的平臺來構建相應的解碼器,默認爲false。所以我們需要設置爲true。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<code class="hljs java"public static PlatformDecoder buildPlatformDecoder(
      PoolFactory poolFactory,
      boolean decodeMemoryFileEnabled) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      return new ArtDecoder(
          poolFactory.getBitmapPool(),
          poolFactory.getFlexByteArrayPoolMaxNumThreads());
    } else {
      if (decodeMemoryFileEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        return new GingerbreadPurgeableDecoder();
      } else {
        return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
      }
    }
  }</code>

4、mDownsampleEnabled——設置EncodeImage解碼時是否解碼圖片樣圖,必須和ImageRequest的ResizeOptions一起使用,作用就是在圖片解碼時根據ResizeOptions所設的寬高的像素進行解碼,這樣解碼出來可以得到一個更小的Bitmap。通過在Decode圖片時,來改變採樣率來實現得,使其採樣ResizeOptions大小。
ResizeOptions和DownsampleEnabled參數都不影響原圖片的大小,影響的是EncodeImage的大小,進而影響Decode出來的Bitmap的大小,ResizeOptions須和此參數結合使用是因爲單獨使用ResizeOptions的話只支持JPEG圖,所以需支持png、jpg、webp需要先設置此參數。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<code class="hljs java">      JobRunnable job = new JobRunnable() {
        @Override
        public void run(EncodedImage encodedImage, boolean isLast) {
          if (encodedImage != null) {
            if (mDownsampleEnabled) {
              ImageRequest request = producerContext.getImageRequest();
              if (mDownsampleEnabledForNetwork ||
                  !UriUtil.isNetworkUri(request.getSourceUri())) {
                encodedImage.setSampleSize(DownsampleUtil.determineSampleSize(
                    request, encodedImage));
              }
            }
            doDecode(encodedImage, isLast);
          }
        }
      };</code>

5、mEncodedMemoryCacheParamsSupplier——編碼圖片的內存緩存。緩存大小默認是通過app運行時最大內存決定的,且最多可存儲getMaxCacheSize()/8個緩存元素,如下代碼:

?
1
2
3
4
5
6
7
8
9
10
<code class="hljs java"private int getMaxCacheSize() {
    final int maxMemory = (int) Math.min(Runtime.getRuntime().maxMemory(), Integer.MAX_VALUE);
    if (maxMemory < 16 * ByteConstants.MB) {
      return 1 * ByteConstants.MB;
    } else if (maxMemory < 32 * ByteConstants.MB) {
      return 2 * ByteConstants.MB;
    } else {
      return 4 * ByteConstants.MB;
    }
  }</code>

想要自己改造那麼就自己實現Supplier接口。
6、mImageCacheStatsTracker——緩存的統計數據追蹤器。它是一個接口,提供了各個緩存中圖片Hit與Miss的回調方法,通常可以使用它來統計緩存命中率,默認情況下Fresco提供了一個NoOp無操作的實現類,我們若是需要使用此功能,必須我們實現ImageCacheStatsTracker接口,在各個回調方法處理。各個方法回調的順序如下:

?
1
2
3
4
5
6
7
8
9
<code class="hljs avrasm">12-25 15:36:35.170 25920-25920/com.sunzxy.myapplication E/zxy: zxy:registerBitmapMemoryCache
12-25 15:36:35.170 25920-25920/com.sunzxy.myapplication E/zxy: zxy:registerEncodedMemoryCache
12-25 15:36:35.330 25920-25920/com.sunzxy.myapplication E/zxy: zxy:onBitmapCacheMiss
12-25 15:36:35.330 25920-25958/com.sunzxy.myapplication E/zxy: zxy:onBitmapCacheMiss
12-25 15:36:35.340 25920-25958/com.sunzxy.myapplication E/zxy: zxy:onMemoryCacheMiss
12-25 15:36:35.340 25920-25960/com.sunzxy.myapplication E/zxy: zxy:onStagingAreaMiss
12-25 15:36:35.360 25920-25960/com.sunzxy.myapplication E/zxy: zxy:onDiskCacheHit
12-25 15:36:35.360 25920-25960/com.sunzxy.myapplication E/zxy: zxy:onMemoryCachePut
12-25 15:36:35.380 25920-25965/com.sunzxy.myapplication E/zxy: zxy:BitmapCachePut</code>

每次請求圖片時,都會走這個路徑,當在各個緩存中hit時,將回調對應得方法,然後在hit時會將圖片分別添加至Encode和Decode緩存。
7、mMainDiskCacheConfig——硬盤緩存的配置,默認緩存目錄在app自身CacheDir的image_cache目錄下,其中設置了三個必備的屬性,分別爲:最大緩存大小、在低內存設備下的緩存大小、在極低內存設備下的緩存大小。默認值爲40M、10M、2M。
這是配置一個DiskCacheConfig的代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
<code class="hljs avrasm">DiskCacheConfig.newBuilder()
        .setBaseDirectoryPathSupplier(
            new Supplier<file>() {
              @Override
              public File get() {
                return context.getApplicationContext().getCacheDir();
              }
            })
        .setBaseDirectoryName("image_cache")
        .setMaxCacheSize(40 * ByteConstants.MB)
        .setMaxCacheSizeOnLowDiskSpace(10 * ByteConstants.MB)
        .setMaxCacheSizeOnVeryLowDiskSpace(2 * ByteConstants.MB)
        .build();</file></code>

8、mMemoryTrimmableRegistry——註冊一個內存調節器,它將根據不同的MemoryTrimType回收類型在需要降低內存使用時候進行回收一些內存緩存資源(Bitmap和Encode)。默認傳入NoOp無操作的一個實現類。
我們自己實現需要實現MemoryTrimmableRegistry接口,然後在它的兩個方法中根據自身需求進行MemoryTrimType的賦值來決定是採取哪個清除策略來回收內存緩存資源。MemoryTrimType是一個枚舉,類型共有四個:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<code class="hljs java">public enum MemoryTrimType {
 
  /** The application is approaching the device-specific Java heap limit. */
  OnCloseToDalvikHeapLimit(0.5),
 
  /** The system as a whole is running out of memory, and this application is in the foreground. */
  OnSystemLowMemoryWhileAppInForeground(0.5),
 
  /** The system as a whole is running out of memory, and this application is in the background. */
  OnSystemLowMemoryWhileAppInBackground(1),
 
  /** This app is moving into the background, usually because the user navigated to another app. */
  OnAppBackgrounded(1);
}</code>

自己實現得內存調節器如下:

?
1
2
3
4
5
6
7
8
9
10
<code class="hljs java">public class MyMemoryTrimmableRegistry implements MemoryTrimmableRegistry {
    @Override
    public void registerMemoryTrimmable(MemoryTrimmable trimmable) {
 
trimmable.trim(MemoryTrimType.OnSystemLowMemoryWhileAppInBackground);
    }
 
    @Override
    public void unregisterMemoryTrimmable(MemoryTrimmable trimmable) {}
}</code>

其中Fresco並沒有對unregister方法進行回調,其中MemoryTrimmable是一個接口,它只有一個方法trim(),就是回收內存緩存資源的,它的實現不需要我們自己寫,而是在CountingMemoryCache類中幫我們實現好了,CountingMemoryCache是一個基於LRU策略來管理緩存中元素的一個類,它實現的trim()方法可以根據Type的不同來採取不同策略的回收爲:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<code class="hljs java"@Override
  public void trim(MemoryTrimType trimType) {
    ArrayList<entry<k, v="">> oldEntries;
    final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType);
    synchronized (this) {
      int targetCacheSize = (int) (mCachedEntries.getSizeInBytes() * (1 - trimRatio));
      int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());
      oldEntries = trimExclusivelyOwnedEntries(Integer.MAX_VALUE, targetEvictionQueueSize);
      makeOrphans(oldEntries);
    }
    maybeClose(oldEntries);
    maybeNotifyExclusiveEntryRemoval(oldEntries);
    maybeUpdateCacheParams();
    maybeEvictEntries();
  }</entry<k,></code>

trim()方法中主要就是做了這麼一件事:根據Type的不同來回收不同比例的內存緩存中最近未被使用的元素。體現在下面這行代碼:

?
1
<code class="hljs glsl">int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());</code>

9、mNetworkFetcher——網絡圖片下載請求類,底層網絡請求默認使用HttpUrlConnection,並且使用一個固定線程數爲3的線程池來管理請求。也可以使用Volley和Okhttp進行擴展,官方默認已經實現好了,如需使用可以參考官方文檔引入額外jar包。
10、mProgressiveJpegConfig——漸進式顯示網絡的JPEG圖的配置,默認傳入一個SimpleProgressiveJpegConfig,通常使用默認配置即可,圖片加載時會從模糊慢慢的到清晰的一個顯示過程,不過要使用漸進式顯示圖片,需要在ImageRequest中顯示的設置是否支持漸進式顯示:

?
1
2
3
<code class="hljs avrasm">ImageRequest request = ImageRequestBuilder
    .setProgressiveRenderingEnabled(true)
    .build();</code>

說明一點,漸進式顯示的效果和漸入圖片顯示效果是兩碼事,漸進式使用的策略是在通過設置統計掃描數,當掃描數大於某個閥值時,然後進行解碼一次並顯示圖片,最後掃描數爲峯值時,進行最後一次解碼,這樣就顯示出清晰的圖片了,也就是說它是採用斷點解碼的。而漸入顯示效果則是通過屬性動畫來改變alpha屬性來顯示圖片的。
11、mResizeAndRotateEnabledForNetwork——最終影響的是mDownsampleEnabledForNetwork參數。
這個參數的作用是在mDownsampleEnabled爲true的情況下,設置是否當這次請求是從網絡中加載圖片時,來對EncodeImage重新改變大小。也就是說設置了這個爲true,可以在從網絡中加載圖片時候根據Resizing參數Decode出更小的樣圖,具體是在Decode時通過採樣Resizing的像素來實現的。

?
1
2
3
4
5
6
7
8
9
10
11
<code class="hljs vbscript">          if (encodedImage != null) {
            if (mDownsampleEnabled) {
              ImageRequest request = producerContext.getImageRequest();
              if (mDownsampleEnabledForNetwork ||
                  !UriUtil.isNetworkUri(request.getSourceUri())) {
                encodedImage.setSampleSize(DownsampleUtil.determineSampleSize(
                    request, encodedImage));
              }
            }
            doDecode(encodedImage, isLast);
          }</code>

這段代碼的意思爲:mDownsampleEnabled爲true的前提下,在第一次加載圖片時候,是從網絡中加載,這時

?
1
<code class="hljs avrasm">!UriUtil.isNetworkUri(request.getSourceUri())</code>

肯定爲false,所以默認不支持對網絡中加載的圖片的EncodeImage的改變。當以後加載該圖片並解析時,由於應用重新啓動或者在後臺中啓動,這時是從硬盤中加載的,這時Uri肯定爲Local的

?
1
<code class="hljs avrasm">!UriUtil.isNetworkUri(request.getSourceUri())</code>

肯定爲true,所以可以從本地加載並解析出sampleBitmap所以設置mDownsampleEnabledForNetwork爲ture,這樣無論是從網絡還是本地都可以改變EncodeImage的大小,在圖片Decode的時候解析出更小的sampleBitmap

12、mSmallImageDiskCacheConfig——小圖的硬盤緩存配置,默認傳入mMainDiskCacheConfig,和主硬盤緩存目錄是共用的。如果需要把小圖和普通圖片分開,則需重新配置。

13、mExecutorSupplier——執行各個任務的線程池配置,包括配置執行IO任務、後臺任務、優先級低的後臺任務、Decode任務的線程池的配置。這些線程池Fresco默認都配置爲Fix固定線程數量的。

mDecodeExecutor——負責圖片解碼成Bitmap的線程池,最大併發數爲CPU的數量。
mIoBoundExecutor——負責從硬盤緩存中讀取緩存圖片的IO線程池,最大併發數爲2。
mBackgroundExecutor——負責後臺的線程任務,一般是負責圖片的resize和旋轉、webp的轉碼、後處理器的執行,最大併發數爲CPU數量。
mLightWeightBackgroundExecutor——低優先級的後臺線程任務,最大併發數爲1。

沒有什麼特別需求,這些後臺任務的線程池配置一般用默認的即可,默認的已經差不多做到極致了。

ImagePipeline

Image pipeline是Fresco中負責圖片加載的,它支持從本地和網絡中加載,文件加載支持:File、content、asset、res目錄下的文件,網絡加載支持http和https,支持的圖片格式有:PNG、GIF、WebP、JPEG。
獲取ImagePipeline可以通過Fresco的靜態工廠方法:

?
1
<code class="hljs fix">ImagePipeline ipl = Fresco.getImagePipeline();</code>

或者通過ImagePipelineFactory的工廠方法:

?
1
<code class="hljs avrasm">ImagePipeline  ipl =ImagePipelineFactory.getInstance().getImagePipeline();</code>

監聽圖片下載進度

?
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
<code class="hljs java">ControllerListener controllerListener = new BaseControllerListener<imageinfo>() {
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) {
      if (imageInfo == null) {
        return;
      }
      QualityInfo qualityInfo = imageInfo.getQualityInfo();
      FLog.d("Final image received! " +
          "Size %d x %d",
          "Quality level %d, good enough: %s, full quality: %s",
          imageInfo.getWidth(),
          imageInfo.getHeight(),
          qualityInfo.getQuality(),
          qualityInfo.isOfGoodEnoughQuality(),
          qualityInfo.isOfFullQuality());
    }
 
    @Override
    public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
      FLog.d("Intermediate image received");
    }
 
    @Override
    public void onFailure(String id, Throwable throwable) {
      FLog.e(getClass(), throwable, "Error loading %s", id)
    }
};
 
Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setControllerListener(controllerListener)
    .setUri(uri);
    // other setters
    .build();
mSimpleDraweeView.setController(controller);</imageinfo></code>

其中三個回調函數:

onFinalImageSet——在加載成功時回調
onFailure——在加載失敗時回調
onIntermediateImageSet——在顯示漸進式JPEG圖片時,這個函數會在每個掃描被解碼後回調

Fresco的多圖請求

假如你需要加載一張大圖,這通常會比較耗時,此時你可以先下載一張縮略圖先顯示,待大圖下載完後則顯示大圖。這樣用戶體驗會好很多。

?
1
2
3
4
5
6
7
<code class="hljs avrasm">Uri lowResUri, highResUri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setLowResImageRequest(ImageRequest.fromUri(lowResUri))
    .setImageRequest(ImageRequest.fromUri(highResUri))
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);</code>

圖片複用

假如有一張圖片,本地和服務端都存在。就比如用戶上傳頭像,會在服務器存一張,而本地也本身就有,所以當加載用戶頭像時,可以設置兩個Uri,本地和網絡,當在本地找到時就不必去服務端加載了,達到複用的效果。

?
1
2
3
4
5
6
7
8
9
10
<code class="hljs avrasm">Uri uri1, uri2;
ImageRequest request = ImageRequest.fromUri(uri1);
ImageRequest request2 = ImageRequest.fromUri(uri2);
ImageRequest[] requests = { request1, request2 };
 
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setFirstAvailableImageRequests(requests)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);</code>

擴展

圖片的縮放和旋轉

1、BitmapFactory.Options的inSampleSize

這種方式對圖片進行解碼壓縮,一系列操作都是通過native層的c/c++代碼進行的,所以進行壓縮過後Bitmap所佔用的空間大小會比原來的小很多。

?
1
2
<code>應用場景:通常使用在圖片的壓縮上,先用inJustDecodeBounds獲取圖片的寬高大小,通過判斷圖片是否過大來進行壓縮。
</code>

2、View的Scale

這種方式的縮放就是通過繪製(Canvas)來實現得,通常我們在動畫方面用的較多,這種方式的特點只是單純的對Bitmap進行放大或縮小的繪製,而實際上Bitmap所佔用的內存空間在Bitmap放大、縮小和原始狀態時的完全一樣,並不能實際的改變內存的佔用。

?
1
2
<code>應用場景:如果圖片本身不大的話,建議使用它,因爲它的速度更快,而且輸出的也是高質量的圖。圖片若是過大,則使用其它。
</code>

3、Fresco的Resizing

通過改變EncodeImage的大小來實現的,使用它重新resize的圖片佔用的內存通常是原始圖片佔用的1/8,目前只支持JPEG格式圖片,所以目前都是與Downsampling結合使用來支持jpg、png、webp格式圖片。

?
1
2
<code>應用場景:ImageRequest中的參數,與ImagePipelineConfig中的Downsampling結合使用,Fresco內置方法。
</code>

4、Fresco的Downsampling

通過在Decode圖片時,來改變採樣率來實現得,使其採樣EncodeImage的sampleSize的大小,這樣Downsampling就變成了Decode過程中的一部分,只需改變Decode過程中對像素點的採樣率就行,而不必新生成一份EncodeImage,而採樣率則是建立在Resizing的寬高大小上,通過Resizing的寬高來決定採樣的部分的像素點。比Resizing更快。

?
1
2
<code>應用場景:和Resizing結合使用,不過在4.4版本中Decode出採樣圖將會佔用更多的內存資源,這是個bug。
</code>

Fresco混淆配置

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<code class="hljs avrasm"># Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
 
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
    @com.facebook.common.internal.DoNotStrip *;
}
 
# Keep native methods
-keepclassmembers class * {
    native <methods>;
}
 
-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn com.android.volley.toolbox.**</methods></code>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章