Glide中bitmap對(duì)象池實(shí)現(xiàn)學(xué)習(xí)

bitmap對(duì)象池基礎(chǔ)代碼學(xué)習(xí)

GroupedLinkedMap.java的邏輯跟java.util.LinkedHashMap類似, 也是通過LRU算法來記錄最近最少使用的數(shù)據(jù). 只不過這里的數(shù)據(jù)指的是尺寸信息, 而不是具體的某個(gè)bitmap對(duì)象. 同時(shí), GroupedLinkedMap把每次get()請(qǐng)求都認(rèn)為是一次訪問, 而忽略addremove操作.

代碼邏輯

對(duì)于其中的雙向鏈表的增長邏輯, 如下圖:

首先有一個(gè)初始節(jié)點(diǎn)Head :


image.png

當(dāng)遇到get(Key)操作時(shí), 會(huì)新生成一個(gè)節(jié)點(diǎn) 并調(diào)用makeHead(entry); 如下圖: 新生成一個(gè)節(jié)點(diǎn)B


image.png

在makeHead()方法的最后會(huì)更新該新節(jié)點(diǎn)B. 最后變成如下:


image.png

于是一次get調(diào)用結(jié)束后, 兩個(gè)節(jié)點(diǎn)的關(guān)系就如上圖

當(dāng)再發(fā)生一次get操作: 會(huì)新生成一個(gè)節(jié)點(diǎn)C , 同樣不管命中與否,都要調(diào)用makeHead()


image.png

makeHead()--> updateEntry()


image.png

其實(shí)是把新節(jié)點(diǎn)插入到head和B節(jié)點(diǎn)之間了, 變換一下上圖, 最后結(jié)果如下:

image.png

為了更清晰的分析其邏輯, 再通過get(key)操作, 插入一個(gè)新節(jié)點(diǎn), 最終圖:

image.png

可以看到head的next始終指向最近被訪問的那個(gè)節(jié)點(diǎn). 而head的prev指向最后被訪問的那個(gè)節(jié)點(diǎn)

于是我們知道, 如果要?jiǎng)h除最后的那個(gè)節(jié)點(diǎn), 就可以通過head立馬找到.

這個(gè)列表結(jié)構(gòu), 應(yīng)該也算是一個(gè)典型的解決方案. 需要記住.

具體分析其removeLast的代碼:


 @Nullable
  public V removeLast() {
    LinkedEntry<K, V> last = head.prev;

    while (!last.equals(head)) {
      V removed = last.removeLast();
      if (removed != null) {
        return removed;
      } else {
        // We will clean up empty lru entries since they are likely to have been one off or
        // unusual sizes and
        // are not likely to be requested again so the gc thrash should be minimal. Doing so will
        // speed up our
        // removeLast operation in the future and prevent our linked list from growing to
        // arbitrarily large
        // sizes.
        removeEntry(last);
        keyToEntry.remove(last.key);
        last.key.offer();
      }

      last = last.prev;
    }

    return null;
  }

如果不看else分支中的特殊邏輯, 那么這個(gè)方法是非常簡單的, 就是通過head節(jié)點(diǎn)的prev找到最不經(jīng)常被訪問的節(jié)點(diǎn), 并返回.

需要注意的是, GroupedLinkedMap中的數(shù)據(jù)除去雙向鏈表的結(jié)構(gòu), 還有一個(gè)map結(jié)構(gòu), value值是LinkedEntry. 而這個(gè)自定義的LinkedEntry, 是持有一個(gè)列表來保存數(shù)據(jù).

問題:

為什么put操作, 會(huì)把新創(chuàng)建的節(jié)點(diǎn)放到隊(duì)尾, 也就是最近不不經(jīng)常使用的位置上. 這是為了什么呢? 難道它的意思是說, 新創(chuàng)建的節(jié)點(diǎn), 一次都沒有被訪問過(get操作) 所以應(yīng)該放在最后的位置上.

--------put操作只會(huì)把新建的節(jié)點(diǎn)放在隊(duì)尾, 因?yàn)檫@個(gè)節(jié)點(diǎn)從來沒有被訪問過, 只要這種類型的數(shù)據(jù)被訪問了, 更具體來說就是被從對(duì)象池中拿去使用了, 那就會(huì)把該節(jié)點(diǎn)前移, 所以如果一直沒人用, 那么這種類型的數(shù)據(jù)在以后遇到內(nèi)存不足時(shí), 會(huì)被優(yōu)先清理掉, 就是因?yàn)闆]人用.

問題:

BitmapPool中存放的bitmap, 或者更具體的說, GroupedLinkedMap中存放的bitmap, 到底是正在被使用的? 還是沒有被使用的?

--------回答這個(gè)問題, 可以通過查看BitmapPool.java這個(gè)接口的方法注釋來解答, put()操作就是把bitmap放回緩沖池, 所以其中的bitap是不在用的.

可以嘗試跟蹤一下, 對(duì)于略懂應(yīng)用, 在請(qǐng)求圖片時(shí), 都是哪些尺寸的圖片請(qǐng)求最多, 如果說圖片的分類比較集中, 那么可以據(jù)此進(jìn)一步優(yōu)化bitmap對(duì)象池.

通過為RequestOptions設(shè)置下面的標(biāo)志, 能夠使得所有的對(duì)象池圖片尺寸都按照控件請(qǐng)求的尺寸, 也就是說如果控件要求一個(gè)200x300的圖片, 那么不管后臺(tái)給的是200x301還是200x400 ,那么都會(huì)只裁剪成200x300, 這樣會(huì)大大減輕bitmap對(duì)象池的占用量和可能導(dǎo)致的回收.

為什么會(huì)影響回收? 是因?yàn)槿绻麑?duì)象池內(nèi)的key, 也就是圖片的分辨率種類太多, 那么對(duì)象池的空間必然會(huì)很快的超過最大限制, 那么進(jìn)而會(huì)觸發(fā)一次對(duì)象池回收, 回收掉最不常用的一種key , 也就是把其對(duì)應(yīng)的列表中的bitmap都進(jìn)行一次recycle回收, 這是很耗時(shí)的. 而且都在主線程中完成.

DEFAULT_REQUESTOPTIONS.getOptions().set(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS, true);

SizeConfigStrategy.java

groupedMap變量定義:

private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();

他的key信息包括尺寸和config.

sortedSizes變量, 定義:

private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>();

key時(shí)Bitmap.Config, value是一個(gè)map, 首先這個(gè)map的定義:

public interface NavigableMap<K,V> extends SortedMap<K,V> {

所以這個(gè)map是排過序的, 當(dāng)然是按key排序的. 它的key是圖片尺寸, 類型為integer, value類型也是integer, 記錄的是圖片尺寸對(duì)應(yīng)的數(shù)量.

sortedSizes到底是什么意義? 理解不了呢!!!!

sortedSizes這個(gè)map的value值雖然聲明為NavigableMap, 但實(shí)際類型為TreeMap


NavigableMap<Integer, Integer> sizes = sortedSizes.get(config);

if (sizes == null) {

sizes = new TreeMap<>();

sortedSizes.put(config, sizes);

}

需要搞清楚這個(gè)bitmap.getConfig()值, 是每個(gè)bitmap各部相同嗎? 還是系統(tǒng)內(nèi)都是一樣的, 類似Bitmap.Config.ARGB_8888 ????

-------回答: Bitmap.Config是一個(gè)emun類型, 當(dāng)然整個(gè)系統(tǒng)內(nèi)只有有限的幾種, 具體到我們的應(yīng)用內(nèi), 只有Bitmap.Config.ARGB_8888 一種. 所以sortedSizes結(jié)構(gòu)就可以解構(gòu)一層, 只需要關(guān)注其中的value類型:NavigableMap<Integer, Integer> sizes

那么這個(gè)類型的sizes有序散列表, 代表了什么含義呢/?>????

這個(gè)有序散列表的key是圖片的大小, 比如1m, 2m, 或者多少K 是一個(gè)數(shù)值.

而這個(gè)有序散列表的vaue是這種大小的數(shù)量. 比如目前整個(gè)bitmap對(duì)象池內(nèi)有1M的圖片3張, 那么value就是3.

在圖片歸還bitmap對(duì)象池時(shí), 需要增加這個(gè)value值, 如果不存在就置為1


@Override
  public void put(Bitmap bitmap) {
    int size = Util.getBitmapByteSize(bitmap);
    Key key = keyPool.get(size, bitmap.getConfig());

    groupedMap.put(key, bitmap);

    NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
    Integer current = sizes.get(key.size);
    sizes.put(key.size, current == null ? 1 : current + 1);
  }

當(dāng)從bitmap對(duì)象池內(nèi)請(qǐng)求圖片后, 需要對(duì)應(yīng)的減少value的計(jì)數(shù)


@Override
  @Nullable
  public Bitmap get(int width, int height, Bitmap.Config config) {
    int size = Util.getBitmapByteSize(width, height, config);
    Key bestKey = findBestKey(size, config);

    Bitmap result = groupedMap.get(bestKey);
    if (result != null) {
      // Decrement must be called before reconfigure.
      decrementBitmapOfSize(bestKey.size, result);
      result.reconfigure(width, height,
          result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
    }
    return result;
  }

計(jì)數(shù)減一的操作都在decrementBitmapOfSize方法中:


 private void decrementBitmapOfSize(Integer size, Bitmap removed) {
    Bitmap.Config config = removed.getConfig();
    NavigableMap<Integer, Integer> sizes = getSizesForConfig(config);
    Integer current = sizes.get(size);
    if (current == null) {
      throw new NullPointerException("Tried to decrement empty size"
          + ", size: " + size
          + ", removed: " + logBitmap(removed)
          + ", this: " + this);
    }

    if (current == 1) {
      sizes.remove(size);
    } else {
      sizes.put(size, current - 1);
    }
  }

如果當(dāng)前只有1個(gè), 再減1就是0, 那么就直接從sizes散列表中刪除這一項(xiàng).

否則就是單純的減1.

這個(gè)邏輯就是比較簡單易懂了.

同理, 當(dāng)bitmap對(duì)象池, 由于占用空間超過最大限制, 而必須刪除一項(xiàng)時(shí), 也需要把sizes中的計(jì)數(shù)減1

  @Override
  @Nullable
  public Bitmap removeLast() {
    Bitmap removed = groupedMap.removeLast();
    if (removed != null) {
      int removedSize = Util.getBitmapByteSize(removed);
      decrementBitmapOfSize(removedSize, removed);
    }
    return removed;
  }

剩下最后一個(gè)難懂的方法findBestKey, 如下圖:


  private Key findBestKey(int size, Bitmap.Config config) {
    Key result = keyPool.get(size, config);
    for (Bitmap.Config possibleConfig : getInConfigs(config)) {
      NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
      Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
      if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
        if (possibleSize != size
            || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
          keyPool.offer(result);
          result = keyPool.get(possibleSize, possibleConfig);
        }
        break;
      }
    }
    return result;
  }

為什么要使用NavigableMap, 是因?yàn)樗幸粋€(gè)celling方法, 對(duì)于根據(jù)size進(jìn)行的查詢可以返回, 一個(gè)大于等于size的bitmap, 這樣仍然可以重用. 而不必強(qiáng)制要求size在map中存在.

說白一點(diǎn): 能找到一個(gè)相同尺寸的bitmap拿來重用當(dāng)然好, 但是如果找不到, 就找一個(gè)稍微大一點(diǎn)的也行. 也能重用.

當(dāng)然, ceilingKey(key) 返回的是大于等于key的, 最接近key的key, 所以只要有等于key的值, 肯定還是優(yōu)先使用key 這個(gè)尺寸本身.

AttributeStrategy.java

在K版本之前的版本上作為LruBitmapPool的具體實(shí)現(xiàn), 邏輯比較簡單了, K版本之前對(duì)bitmap的復(fù)用, 要求比較高, 只有尺寸完全一致的bitmap才能被復(fù)用, 所以其內(nèi)部實(shí)現(xiàn)很簡單, 就是通過GroupedLinkedMap來保存bitmap, 可以是bitmap 對(duì)應(yīng)的key(size, config).

LruBitmapPool.java

根據(jù)版本來選擇對(duì)應(yīng)的緩存策略實(shí)現(xiàn)類:


  private static LruPoolStrategy getDefaultStrategy() {
    final LruPoolStrategy strategy;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      strategy = new SizeConfigStrategy();
    } else {
      strategy = new AttributeStrategy();
    }
    return strategy;
  }

dump()

dumpUnchecked()

getDefaultAllowedConfigs()

ThrowingBitmapTracker

NullBitmapTracker

---------------這幾個(gè)都可以先不看, 都沒用.

唯一要關(guān)注的是put() 和 get()方法.

實(shí)際上對(duì)bitmap的緩存都是通過private final LruPoolStrategy strategy; 來委托實(shí)現(xiàn)的.

最復(fù)雜的方法trimToSize() 從優(yōu)化的經(jīng)歷來看這里面的 removed.recycle()操作非常耗時(shí). 能在非UI線程recycle嗎?


private synchronized void trimToSize(long size) {
    while (currentSize > size) {
      final Bitmap removed = strategy.removeLast();
      // TODO: This shouldn't ever happen, see #331.
      if (removed == null) {
        if (Log.isLoggable(TAG, Log.WARN)) {
          Log.w(TAG, "Size mismatch, resetting");
          dumpUnchecked();
        }
        currentSize = 0;
        return;
      }
      tracker.remove(removed);
      currentSize -= strategy.getSize(removed);
      evictions++;
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
      }
      dump();
      removed.recycle();
    }
  }

我們可以想辦法減少trimToSize()的執(zhí)行, 從而避免在ui線程回收bitmap導(dǎo)致的耗時(shí).

bitmapPool等緩存池的大小的設(shè)置, 是通過MemorySizeCalculator來設(shè)置的.

可以看到對(duì)于O版本以上的系統(tǒng), 設(shè)置的bitmappool尺寸會(huì)比較小.

private final int bitmapPoolSize;

int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;

int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);


image.png

如果是O版本之前的系統(tǒng), 就是4屏幕的圖片大小空間作為緩存, 而從O版本開始就只有1屏幕了. 原因看上面的注釋.

但是還有一點(diǎn),如果是高于O版本的低內(nèi)存設(shè)備, 那么不會(huì)設(shè)置圖片bitmap緩存, 會(huì)創(chuàng)建一個(gè)BitmapPoolAdapter實(shí)例作為一個(gè)虛緩存, 其實(shí)每次取bitmap時(shí)都是create操作.


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLowMemoryDevice(activityManager)) {

    bitmapPoolScreens = 0;

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谤职,一起剝皮案震驚了整個(gè)濱河市耻蛇,隨后出現(xiàn)的幾起案子便脊,更是在濱河造成了極大的恐慌胶台,老刑警劉巖假褪,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件署咽,死亡現(xiàn)場離奇詭異,居然都是意外死亡生音,警方通過查閱死者的電腦和手機(jī)宁否,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缀遍,“玉大人慕匠,你說我怎么就攤上這事∮虼迹” “怎么了台谊?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長譬挚。 經(jīng)常有香客問我锅铅,道長,這世上最難降的妖魔是什么减宣? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任盐须,我火速辦了婚禮,結(jié)果婚禮上漆腌,老公的妹妹穿的比我還像新娘贼邓。我一直安慰自己阶冈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布塑径。 她就那樣靜靜地躺著女坑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪晓勇。 梳的紋絲不亂的頭發(fā)上堂飞,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音绑咱,去河邊找鬼绰筛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛描融,可吹牛的內(nèi)容都是我干的铝噩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼窿克,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼骏庸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起年叮,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤具被,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后只损,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體一姿,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年跃惫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叮叹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爆存,死狀恐怖蛉顽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情先较,我是刑警寧澤携冤,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站闲勺,受9級(jí)特大地震影響噪叙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜霉翔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望苞笨。 院中可真熱鬧债朵,春花似錦子眶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谚中,卻和暖如春渴杆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宪塔。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國打工磁奖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人某筐。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓比搭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親南誊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子身诺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容