[Glide系列第3篇]Glide源碼分析之緩存處理

Android緩存機(jī)制:如果沒(méi)有緩存,在大量的網(wǎng)絡(luò)請(qǐng)求從遠(yuǎn)程獲取圖片時(shí)會(huì)造成網(wǎng)絡(luò)流量的浪費(fèi),加載速度較慢,用戶體驗(yàn)不好

關(guān)于學(xué)習(xí)Glide緩存原理前十分建議你先了解圖片加載的流程,在這基礎(chǔ)上再進(jìn)行學(xué)習(xí)會(huì)更加上手。然后可以看思維導(dǎo)圖,從宏觀角度理解Glide加載。本文的源碼基于V4版本

Glide系列文章

Glide源碼分析流程思維導(dǎo)圖

【兩篇就懂系列】Glide源碼分析之加載圖片流程(1/2)

【兩篇就懂系列】Glide源碼分析之加載圖片流程(2/2)

Glide圖片加載庫(kù)從v3遷移到v4的改變和使用


【Glide的緩存】

在緩存這一功能上,Glide將它分成了兩個(gè)模塊,一個(gè)是內(nèi)存緩存,一個(gè)是硬盤緩存。同時(shí)內(nèi)存緩存又分為兩級(jí),一級(jí)是LruCache緩存,一級(jí)是弱引用緩存。
  • 內(nèi)存緩存的作用:防止應(yīng)用重復(fù)將圖片數(shù)據(jù)讀取到內(nèi)存當(dāng)中。
    • LruCache緩存:不在使用中的圖片使用LruCache來(lái)進(jìn)行緩存。
    • 弱引用緩存:把正在使用中的圖片使用弱引用來(lái)進(jìn)行緩存。
      【這樣的目的保護(hù)正在使用的資源不會(huì)被LruCache算法回收。】
  • 硬盤緩存的作用:防止應(yīng)用重復(fù)從網(wǎng)絡(luò)或其他地方重復(fù)下載和讀取數(shù)據(jù)。

默認(rèn)情況下,Glide 會(huì)在開(kāi)始一個(gè)新的圖片請(qǐng)求之前檢查以下多級(jí)的緩存:

  • 內(nèi)存緩存 (Memory cache) - 該圖片是否最近被加載過(guò)并仍存在于內(nèi)存中?即LruCache緩存。
  • 活動(dòng)資源 (Active Resources) - 現(xiàn)在是否有另一個(gè) View 正在展示這張圖片?也就是弱引用緩存。
  • 資源類型(Resource) - 該圖片是否之前曾被解碼、轉(zhuǎn)換并寫入過(guò)磁盤緩存?
  • 數(shù)據(jù)來(lái)源 (Data) - 構(gòu)建這個(gè)圖片的資源是否之前曾被寫入過(guò)文件緩存?
    前兩步檢查圖片是否在內(nèi)存中,如果是則直接返回圖片。后兩步則檢查圖片是否在磁盤上,以便快速但異步地返回圖片。
    如果四個(gè)步驟都未能找到圖片,則Glide會(huì)返回到原始資源以取回?cái)?shù)據(jù)(原始文件,Uri, Url等)

結(jié)合源碼去分析Glide緩存

首先在[圖片加載源碼分析一]的文章中,我們?cè)谕ㄟ^(guò)單例獲取Glide的實(shí)例時(shí),調(diào)用過(guò)checkAndInitializeGlide(context)這個(gè)方法,在具體的方法里有一段代碼是通過(guò)GlideBuilder.build初始化一些對(duì)象,如下

GlideBuilder類
public Glide build(Context context) {
    if (sourceExecutor == null) {
      sourceExecutor = GlideExecutor.newSourceExecutor();
    }

    if (diskCacheExecutor == null) {
      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
    }

    if (memorySizeCalculator == null) {
      memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
    }

    if (connectivityMonitorFactory == null) {
      connectivityMonitorFactory = new DefaultConnectivityMonitorFactory();
    }

    if (bitmapPool == null) {
      int size = memorySizeCalculator.getBitmapPoolSize();
      if (size > 0) {
        bitmapPool = new LruBitmapPool(size);
      } else {
        bitmapPool = new BitmapPoolAdapter();
      }
    }

    if (arrayPool == null) {
      arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes());
    }
  //1
    if (memoryCache == null) {
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }
  //2
    if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }

    if (engine == null) {
      engine =
          new Engine(
              memoryCache,
              diskCacheFactory,
              diskCacheExecutor,
              sourceExecutor,
              GlideExecutor.newUnlimitedSourceExecutor(),
              GlideExecutor.newAnimationExecutor());
    }

    RequestManagerRetriever requestManagerRetriever = new RequestManagerRetriever(
        requestManagerFactory);

    return new Glide(
        context,
        engine,
        memoryCache,
        bitmapPool,
        arrayPool,
        requestManagerRetriever,
        connectivityMonitorFactory,
        logLevel,
        defaultRequestOptions.lock(),
        defaultTransitionOptions);
  }

在我代碼中標(biāo)記1和2處,1處new出了一個(gè)LruResourceCache,并把它賦值到了memoryCache這個(gè)對(duì)象上面。你沒(méi)有猜錯(cuò),這個(gè)就是Glide實(shí)現(xiàn)內(nèi)存緩存所使用的LruCache對(duì)象了。2處InternalCacheDiskCacheFactory是磁盤緩存(內(nèi)部存儲(chǔ))所使用的工廠對(duì)象。同時(shí)在其中初始化了磁盤緩存的大小和文件的路徑。

創(chuàng)建好了這些對(duì)象說(shuō)明我們已經(jīng)把準(zhǔn)備工作做好了。

【內(nèi)存緩存】

接口為MemoryCache,Glide使用LruResourceCache作為默認(rèn)的內(nèi)存緩存,該類是接口MemoryCache的一個(gè)缺省實(shí)現(xiàn)(接口的另一個(gè)實(shí)現(xiàn)類為MemoryCacheAdapter)。使用固定大小的內(nèi)存和 LRU 算法。LruResourceCache的大小由 Glide 的MemorySizeCalculator類來(lái)決定,這個(gè)類主要關(guān)注設(shè)備的內(nèi)存類型,設(shè)備 RAM 大小,以及屏幕分辨率。

【內(nèi)存緩存的讀取】

我們先分析Glide從哪里讀取內(nèi)存緩存,以及內(nèi)存緩存的原理。

1. Lurcache算法
對(duì)于大多內(nèi)存緩存的實(shí)現(xiàn),我們通常會(huì)知道這樣一個(gè)算法,LruCache算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的對(duì)象用強(qiáng)引用存儲(chǔ)在LinkedHashMap(雙向循環(huán)列表)中,并且把最近最少使用的對(duì)象在緩存值達(dá)到預(yù)設(shè)定值之前從內(nèi)存中移除。淘汰最長(zhǎng)時(shí)間未使用的對(duì)象
上面我們提到的LruResourceCache就是Glide實(shí)現(xiàn)內(nèi)存緩存所使用的LruCache對(duì)象了,而這個(gè)類繼承LruCache。也是最近最少使用算法的具體實(shí)現(xiàn)。
public class LruCache<T, Y> {
  private final LinkedHashMap<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
  private final int initialMaxSize;
  private int maxSize;
  private int currentSize = 0;

  /**
   * Constructor for LruCache.
   *
   * @param size The maximum size of the cache, the units must match the units used in {@link
   *             #getSize(Object)}.
   */
  public LruCache(int size) {
    this.initialMaxSize = size;
    this.maxSize = size;
  }

  /**
   * Sets a size multiplier that will be applied to the size provided in the constructor to put the
   * new size of the cache. If the new size is less than the current size, entries will be evicted
   * until the current size is less than or equal to the new size.
   *
   * @param multiplier The multiplier to apply.
   */
  public synchronized void setSizeMultiplier(float multiplier) {
    if (multiplier < 0) {
      throw new IllegalArgumentException("Multiplier must be >= 0");
    }
    maxSize = Math.round(initialMaxSize * multiplier);
    evict();
  }

  /**
   * Returns the size of a given item, defaulting to one. The units must match those used in the
   * size passed in to the constructor. Subclasses can override this method to return sizes in
   * various units, usually bytes.
   *
   * @param item The item to get the size of.
   */
  protected int getSize(Y item) {
    return 1;
  }

  /**
   * Returns the number of entries stored in cache.
   */
  protected synchronized int getCount() {
    return cache.size();
  }

  /**
   * A callback called whenever an item is evicted from the cache. Subclasses can override.
   *
   * @param key  The key of the evicted item.
   * @param item The evicted item.
   */
  protected void onItemEvicted(T key, Y item) {
    // optional override
  }

  /**
   * Returns the current maximum size of the cache in bytes.
   */
  public synchronized int getMaxSize() {
    return maxSize;
  }

  /**
   * Returns the sum of the sizes of all items in the cache.
   */
  public synchronized int getCurrentSize() {
    return currentSize;
  }

  /**
   * Returns true if there is a value for the given key in the cache.
   *
   * @param key The key to check.
   */

  public synchronized boolean contains(T key) {
    return cache.containsKey(key);
  }

  /**
   * Returns the item in the cache for the given key or null if no such item exists.
   *
   * @param key The key to check.
   */
  @Nullable
  public synchronized Y get(T key) {
    return cache.get(key);
  }

  /**
   * Adds the given item to the cache with the given key and returns any previous entry for the
   * given key that may have already been in the cache.
   *
   * <p> If the size of the item is larger than the total cache size, the item will not be added to
   * the cache and instead {@link #onItemEvicted(Object, Object)} will be called synchronously with
   * the given key and item. </p>
   *
   * @param key  The key to add the item at.
   * @param item The item to add.
   */
  public synchronized Y put(T key, Y item) {
    final int itemSize = getSize(item);
    if (itemSize >= maxSize) {
      onItemEvicted(key, item);
      return null;
    }

    final Y result = cache.put(key, item);
    if (item != null) {
      currentSize += getSize(item);
    }
    if (result != null) {
      // TODO: should we call onItemEvicted here?
      currentSize -= getSize(result);
    }
    evict();

    return result;
  }

  /**
   * Removes the item at the given key and returns the removed item if present, and null otherwise.
   *
   * @param key The key to remove the item at.
   */
  @Nullable
  public synchronized Y remove(T key) {
    final Y value = cache.remove(key);
    if (value != null) {
      currentSize -= getSize(value);
    }
    return value;
  }

  /**
   * Clears all items in the cache.
   */
  public void clearMemory() {
    trimToSize(0);
  }

  /**
   * Removes the least recently used items from the cache until the current size is less than the
   * given size.
   *
   * @param size The size the cache should be less than.
   */
  protected synchronized void trimToSize(int size) {
    Map.Entry<T, Y> last;
    while (currentSize > size) {
      last = cache.entrySet().iterator().next();
      final Y toRemove = last.getValue();
      currentSize -= getSize(toRemove);
      final T key = last.getKey();
      cache.remove(key);
      onItemEvicted(key, toRemove);
    }
  }

  private void evict() {
    trimToSize(maxSize);
  }
}
2. Glide內(nèi)存緩存的實(shí)現(xiàn)自然也是使用的LruCache算法。不過(guò)除了LruCache算法之外,Glide還結(jié)合了一種弱引用的機(jī)制,共同完成了內(nèi)存緩存功能。這樣做的目的是把正在使用中的圖片使用弱引用來(lái)進(jìn)行緩存,不在使用中的圖片使用LruCache來(lái)進(jìn)行緩存的功能。分工合作,保護(hù)正在使用的資源不會(huì)被LruCache算法回收掉。(劃重點(diǎn))
3. Glide默認(rèn)情況下,Glide自動(dòng)就是開(kāi)啟內(nèi)存緩存的。也就是說(shuō),當(dāng)我們使用Glide加載了一張圖片之后,這張圖片就會(huì)被緩存到內(nèi)存當(dāng)中,只要在它還沒(méi)從內(nèi)存中被清除之前,下次使用Glide再加載這張圖片都會(huì)直接從內(nèi)存當(dāng)中讀取,而不用重新從網(wǎng)絡(luò)或硬盤上讀取了,這樣無(wú)疑就可以大幅度提升圖片的加載效率。比方我們?cè)谑褂肦ecyclerView、listview、viewpager這種控件時(shí),反復(fù)上下滑動(dòng),當(dāng)移出屏幕的項(xiàng)被回收再次移入屏幕展示時(shí),那么只要是Glide加載過(guò)的圖片都可以直接從內(nèi)存當(dāng)中迅速讀取并展示出來(lái)。
4. 看過(guò)了之前圖片加載的兩篇文章,我們?cè)诘谌絠nto時(shí),onSizeReady準(zhǔn)備圖片加載時(shí),會(huì)調(diào)用Engine.load這個(gè)比較重要的方法,在上一篇文章分析時(shí),我們忽略了對(duì)緩存的處理,而是直接分析沒(méi)有緩存的加載過(guò)程。而這篇文章我們返回看緩存處理。
重新放出Engine.load方法。
Engine類
public <R> LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      Class<?> resourceClass,
      Class<R> transcodeClass,
      Priority priority,
      DiskCacheStrategy diskCacheStrategy,
      Map<Class<?>, Transformation<?>> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb) {
    Util.assertMainThread();
    long startTime = LogTime.getLogTime();
  //a
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);
  //b
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return null;
    }
  //c
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return null;
    }
  //d
    EngineJob<?> current = jobs.get(key);
    if (current != null) {
      current.addCallback(cb);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Added to existing load", startTime, key);
      }
      return new LoadStatus(cb, current);
    }
  //e
    EngineJob<R> engineJob = engineJobFactory.build(key, isMemoryCacheable,
        useUnlimitedSourceExecutorPool, useAnimationPool);
    DecodeJob<R> decodeJob = decodeJobFactory.build(
        glideContext,
        model,
        key,
        signature,
        width,
        height,
        resourceClass,
        transcodeClass,
        priority,
        diskCacheStrategy,
        transformations,
        isTransformationRequired,
        isScaleOnlyOrNoTransform,
        onlyRetrieveFromCache,
        options,
        engineJob);
    jobs.put(key, engineJob);
    engineJob.addCallback(cb);
    engineJob.start(decodeJob);

    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      logWithTimeAndKey("Started new load", startTime, key);
    }
    return new LoadStatus(cb, engineJob);
  }
  • //a處:首先通過(guò)KeyFactory的buildKey方法創(chuàng)建了一個(gè)EngineKey對(duì)象(緩存鍵),這個(gè)對(duì)象就是我們說(shuō)的緩存key,加載資源的唯一標(biāo)識(shí)??梢钥吹?jīng)Q定緩存Key的條件非常多,即使你用override()方法改變了一下圖片的width或者h(yuǎn)eight,也會(huì)生成一個(gè)完全不同的緩存Key。
  • //b處:通過(guò)loadFromCache方法,通過(guò)key查找緩存資源,此時(shí)的緩存為內(nèi)存緩存,如果獲取的到就直接調(diào)用cb.onResourceReady()方法進(jìn)行回調(diào)。
  • //c處:如果內(nèi)存緩存沒(méi)有找到對(duì)應(yīng)key的資源,則調(diào)用loadFromActiveResources方法,還是通過(guò)key獲取緩存資源,而此時(shí)的緩存也是內(nèi)存緩存。獲取到的話也直接進(jìn)行回調(diào)。
也就是說(shuō),Glide的圖片加載過(guò)程中會(huì)調(diào)用兩個(gè)方法來(lái)獲取內(nèi)存緩存,loadFromCache()和loadFromActiveResources()。這兩個(gè)方法中前者使用的就是LruCache算法,后者使用的就是弱引用。
  • //d處:如果以上都沒(méi)有找到,那是否可能該緩存任務(wù)正在處理,還沒(méi)有完成緩存,所以根據(jù)key判斷緩存的job中是否有current,如果有,就不用新創(chuàng)建任務(wù)了,而是給其添加回調(diào),等待完成后獲取。
  • //e處:如果以上條件都不滿足,我們就需要?jiǎng)?chuàng)建新的加載任務(wù)。并把當(dāng)前任務(wù)存放在jobs這個(gè)map中。同時(shí)要開(kāi)啟線程來(lái)加載新的圖片了。
5.看一下loadFromCache()和loadFromActiveResources()這兩個(gè)方法的源碼:
//Engine類
  private final MemoryCache cache;
  private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
...省略
//內(nèi)存緩存 (Memory cache) - 該圖片是否最近被加載過(guò)并仍存在于內(nèi)存中?
 private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
  //a
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
  //c
      cached.acquire();
  //d
      activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
    }
    return cached;
  }

  @SuppressWarnings("unchecked")
  private EngineResource<?> getEngineResourceFromCache(Key key) {
  //b
    Resource<?> cached = cache.remove(key);

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      // Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource<?>) cached;
    } else {
      result = new EngineResource<>(cached, true /*isMemoryCacheable*/);
    }
    return result;
  }
//活動(dòng)資源 (Active Resources) - 現(xiàn)在是否有另一個(gè) View 正在展示這張圖片?
 //e
  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> active = null;
    WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
    if (activeRef != null) {
      active = activeRef.get();//得到引用的資源
  //再次判斷是為了防止引用被清空,或gc回收
      if (active != null) {
        active.acquire();
      } else {
        activeResources.remove(key);
      }
    }

    return active;
  }
  • //a處:首先關(guān)于傳入?yún)?shù)isMemoryCacheable,代表內(nèi)存緩存是否被開(kāi)啟,Glide默認(rèn)為開(kāi)啟,true。但如果想要禁用的話呢?通過(guò)向skipMemoryCache()傳入true,此時(shí)isMemoryCacheable將為false,返回值也為null。Glide-v3到v4寫法的變化
GlideApp.with(fragment)
  .load(url)
  .skipMemoryCache(true)
  .into(view);
  • //b處:接著調(diào)用了getEngineResourceFromCache(key)方法來(lái)獲取緩存。在這個(gè)方法中,會(huì)使用緩存Key來(lái)從cache當(dāng)中取值,而這里的cache對(duì)象就是在構(gòu)建Glide對(duì)象時(shí)創(chuàng)建的LruResourceCache,那么說(shuō)明這里其實(shí)使用的就是LruCache算法了。當(dāng)我們從LruResourceCache中獲取到緩存圖片之后會(huì)將它從緩存中移除。cache.remove(key)。這個(gè)語(yǔ)句既返回了對(duì)應(yīng)key的value值,也將對(duì)應(yīng)的key從cache中移除。
  • //c處:如果cached不為null,首先調(diào)用cached.acquire();EngineResource用一個(gè)acquired變量來(lái)記錄圖片被引用的次數(shù),調(diào)用acquire()方法會(huì)讓變量acquire加1,調(diào)用release()方法會(huì)讓變量減1。(當(dāng)acquired變量大于0的時(shí)候,說(shuō)明圖片正在使用中,也就應(yīng)該放到activeResources弱引用緩存當(dāng)中,如果acquired變量等于0了,說(shuō)明圖片已經(jīng)不在使用中了。釋放資源,從activeResources弱引用緩存中移除,并put到LruResourceCache當(dāng)中)
  • //d處:然后將這個(gè)緩存圖片存儲(chǔ)到activeResources當(dāng)中。activeResources就是一個(gè)弱引用的HashMap,用來(lái)緩存正在使用中的圖片,我們可以看到,loadFromActiveResources()方法就是從activeResources這個(gè)HashMap當(dāng)中取值的。使用activeResources來(lái)緩存正在使用中的圖片,可以保護(hù)這些圖片不會(huì)被LruCache算法回收掉。
  • //e處:如果從內(nèi)存中沒(méi)有找到資源,那有一種可能為該資源已被LruCache算法移除,但是它正在被另一個(gè)view展示,所以此時(shí)還是有此資源的緩存。所以查找,存在,引用值加1,不存在,則把key從activeResources弱引用緩存中移除。

【內(nèi)存緩存的寫入】

1.當(dāng)沒(méi)有讀取到緩存時(shí),我們肯定要正常開(kāi)啟線程去下載資源了,具體流程可以看之前的文章,那么從服務(wù)端得到資源后是何時(shí)以及如何寫入到緩存中呢?下面來(lái)具體分析:

上一篇文章講解過(guò),當(dāng)從服務(wù)端得到stream然后做處理得到的最終圖片資源通過(guò)層層回調(diào)返回等最終交給EngineJob的onResourceReady的方法處理。而在這個(gè)方法中通過(guò)Handler發(fā)送一條消息將執(zhí)行邏輯切回到主線程當(dāng)中,從而執(zhí)行handleResultOnMainThread()方法。

EngineJob類
...
  private final EngineJobListener listener;
... 
 void handleResultOnMainThread() {
    stateVerifier.throwIfRecycled();
    if (isCancelled) {
      resource.recycle();
      release(false /*isRemovedFromQueue*/);
      return;
    } else if (cbs.isEmpty()) {
      throw new IllegalStateException("Received a resource without any callbacks to notify");
    } else if (hasResource) {
      throw new IllegalStateException("Already have resource");
    }
    engineResource = engineResourceFactory.build(resource, isCacheable);
    hasResource = true;

    // Hold on to resource for duration of request so we don't recycle it in the middle of
    // notifying if it synchronously released by one of the callbacks.
    engineResource.acquire();
    listener.onEngineJobComplete(key, engineResource);

    for (ResourceCallback cb : cbs) {
      if (!isInIgnoredCallbacks(cb)) {
        engineResource.acquire();
        cb.onResourceReady(engineResource, dataSource);
      }
    }
    // Our request is complete, so we can release the resource.
    engineResource.release();

    release(false /*isRemovedFromQueue*/);
  }

在這個(gè)方法里,通過(guò)EngineResourceFactory構(gòu)建出了一個(gè)包含圖片資源的EngineResource對(duì)象,然后將這個(gè)對(duì)象回調(diào)到Engine的onEngineJobComplete()方法當(dāng)中.

Engine類
public class Engine implements EngineJobListener,
    MemoryCache.ResourceRemovedListener,
    EngineResource.ResourceListener {
...
  @SuppressWarnings("unchecked")
  @Override
  public void onEngineJobComplete(Key key, EngineResource<?> resource) {
    Util.assertMainThread();
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null) {
      resource.setResourceListener(key, this);

      if (resource.isCacheable()) {
        activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
      }
    }
    // TODO: should this check that the engine job is still current?
    jobs.remove(key);
  }
}
重點(diǎn):先將resource添加監(jiān)聽(tīng),然后回調(diào)過(guò)來(lái)的EngineResource被put到了activeResources當(dāng)中,在這里寫入到了內(nèi)存緩存的弱引用緩存。寫入到弱引用緩存的原因是這個(gè)資源是屬于正在被加載展示的資源,也就是正在被使用的資源。
那什么時(shí)候要寫入到內(nèi)存緩存的LruCache中呢?我們說(shuō)過(guò)要將不在使用中的圖片使用LruCache來(lái)進(jìn)行緩存,那怎么判斷是否在使用中?那就是前面講到的EngineResource中的一個(gè)引用機(jī)制,通過(guò)acquired的值來(lái)判斷。(當(dāng)acquired變量大于0的時(shí)候,說(shuō)明圖片正在使用中,也就應(yīng)該放到activeResources弱引用緩存當(dāng)中,如果acquired變量等于0了,說(shuō)明圖片已經(jīng)不在使用中了,此時(shí)放到LruCache來(lái)進(jìn)行緩存。)acquired的增加和減少通過(guò)EngineResource的acquire()和release()方法。
EngineResource類
class EngineResource<Z> implements Resource<Z> {
  private int acquired;
  private ResourceListener listener;
  ...
  void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
    if (!Looper.getMainLooper().equals(Looper.myLooper())) {
      throw new IllegalThreadStateException("Must call acquire on the main thread");
    }
    ++acquired;
  }
  void release() {
    if (acquired <= 0) {
      throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
    }
    if (!Looper.getMainLooper().equals(Looper.myLooper())) {
      throw new IllegalThreadStateException("Must call release on the main thread");
    }
    if (--acquired == 0) {
      listener.onResourceReleased(key, this);
    }
  }
}
在release方法可以看到,當(dāng)acquired=0時(shí),調(diào)用engine的onResourceReleased();
Engine類
  @Override
  public void onResourceReleased(Key cacheKey, EngineResource resource) {
    Util.assertMainThread();
    activeResources.remove(cacheKey);
    if (resource.isCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
  }
當(dāng)不在使用中時(shí),此時(shí)就可以從 activeResources移除,同時(shí)就可以添加到Lrucache中了。此處為寫入內(nèi)存緩存的LruCache地方。
截止到此 內(nèi)存緩存分析完畢。

【磁盤緩存】

接口為DiskCache,Glide 使用DiskLruCacheWrapper作為默認(rèn)的磁盤緩存,該類是接口MemoryCache的實(shí)現(xiàn)類(該接口的另一個(gè)實(shí)現(xiàn)類為DiskCacheAdapter)。 DiskLruCacheWrapper是一個(gè)使用 LRU 算法的固定大小的磁盤緩存。默認(rèn)磁盤大小為250MB,位置是在應(yīng)用的緩存文件夾 下中的一個(gè) 特定目錄 。
Google也曾提供了一個(gè)現(xiàn)成的工具類,DiskLruCache。郭霖大神這篇文章對(duì)這個(gè)DiskLruCache工具進(jìn)行了比較全面的分析,感興趣的朋友可以參考一下 Android DiskLruCache完全解析,硬盤緩存的最佳方案
默認(rèn)情況下我們進(jìn)行初始化glide時(shí)是磁盤內(nèi)部存儲(chǔ)new InternalCacheDiskCacheFactory(context),假如應(yīng)用程序展示的媒體內(nèi)容是公開(kāi)的(從無(wú)授權(quán)機(jī)制的網(wǎng)站上加載,或搜索引擎等),那么應(yīng)用可以將這個(gè)緩存位置改到外部存儲(chǔ):在自定義moudle的配置GlideBuilder.setDiskCache(new ExternalDiskCacheFactory(context));
無(wú)論使用內(nèi)部或外部磁盤緩存,應(yīng)用程序都可以改變磁盤緩存的大小和改變緩存文件夾在外存或內(nèi)存上的名字:
官網(wǎng)示例:
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    int diskCacheSizeBytes = 1024  1024  100;  100 MB
    builder.setDiskCache(
        new InternalDiskCacheFactory(context, cacheFolderName, diskCacheSizeBytes));
  }
}
上面我們提到過(guò)內(nèi)存緩存可以禁止,同樣磁盤緩存也可以。
GlideApp.with(fragment)
  .load(url)
  .diskCacheStrategy(DiskCacheStrategy.NONE)
  .into(view);

【磁盤緩存策略】

看到diskCacheStrategy()方法,我們就必須要提一下磁盤緩存策略:DiskCacheStrategy可被diskCacheStrategy方法應(yīng)用到每一個(gè)單獨(dú)的請(qǐng)求。 目前支持的策略如下
  • DiskCacheStrategy.ALL : 表示既緩存原始圖片,也緩存轉(zhuǎn)換過(guò)后的圖片。對(duì)于遠(yuǎn)程圖片,緩存DATA和RESOURCE。對(duì)于本地圖片,只緩存RESOURCE。
  • DiskCacheStrategy.AUTOMATIC :它會(huì)嘗試對(duì)本地和遠(yuǎn)程圖片使用最佳的策略。當(dāng)你加載遠(yuǎn)程數(shù)據(jù)(比如,從URL下載)時(shí),AUTOMATIC 策略僅會(huì)存儲(chǔ)未被你的加載過(guò)程修改過(guò)(比如,變換,裁剪–譯者注)的原始數(shù)據(jù)(DATA),因?yàn)橄螺d遠(yuǎn)程數(shù)據(jù)相比調(diào)整磁盤上已經(jīng)存在的數(shù)據(jù)要昂貴得多。對(duì)于本地?cái)?shù)據(jù),AUTOMATIC 策略則會(huì)僅存儲(chǔ)變換過(guò)的縮略圖(RESOURCE),因?yàn)榧词鼓阈枰俅紊闪硪粋€(gè)尺寸或類型的圖片,取回原始數(shù)據(jù)也很容易。
  • DiskCacheStrategy.DATA:表示只緩存未被處理的文件。我的理解就是我們獲得的stream。它是不會(huì)被展示出來(lái)的,需要經(jīng)過(guò)裝載decode,對(duì)圖片進(jìn)行壓縮和轉(zhuǎn)換,等等操作,得到最終的圖片才能被展示。
  • DiskCacheStrategy.NONE: 表示不緩存任何內(nèi)容。
  • DiskCacheStrategy.RESOURCE:表示只緩存轉(zhuǎn)換過(guò)后的圖片。(也就是經(jīng)過(guò)decode,轉(zhuǎn)化裁剪的圖片)
默認(rèn)的策略為DiskCacheStrategy.AUTOMATIC,改變策略也很簡(jiǎn)單, xxx.diskCacheStrategy(DiskCacheStrategy.ALL);

【磁盤緩存的讀取】

上面講過(guò)內(nèi)存緩存的讀取,那磁盤緩存是在哪里讀取的呢?和內(nèi)存緩存一樣,我們觸發(fā)圖片的加載是在Engine的load方法中,當(dāng)我們從內(nèi)存緩存以及當(dāng)前任務(wù)中都沒(méi)有找到資源時(shí),我們要開(kāi)啟線程去下載,engineJob.start(decodeJob);上一篇文章因?yàn)楹雎跃彺妫紤]加載,所以當(dāng)時(shí)忽略緩存操作,而這次我們帶著緩存一起看。
EngineJob類
public void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    GlideExecutor executor = decodeJob.willDecodeFromCache()
        ? diskCacheExecutor
        : getActiveSourceExecutor();
    executor.execute(decodeJob);
  }
DecodeJob類
  /**
   * Returns true if this job will attempt to decode a resource from the disk cache, and false if it
   * will always decode from source.
   */
  boolean willDecodeFromCache() {
    Stage firstStage = getNextStage(Stage.INITIALIZE);
    return firstStage == Stage.RESOURCE_CACHE || firstStage == Stage.DATA_CACHE;
  }

  private Stage getNextStage(Stage current) {
    switch (current) {
      case INITIALIZE:
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);
      case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);
      case DATA_CACHE:
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

  • willDecodeFromCache()方法通過(guò)調(diào)用getNextStage,傳入初始化標(biāo)識(shí)INITIALIZE,得到當(dāng)前階段標(biāo)識(shí),diskCacheStrategy.decodeCachedResource()返回一個(gè)boolean標(biāo)識(shí),如果我們指定磁盤緩存策略為DiskCacheStrategy.ALL或DiskCacheStrategy.RESOURCE或由DiskCacheStrategy.AUTOMATIC對(duì)遠(yuǎn)程圖片使用磁盤緩存時(shí),此時(shí)返回true,標(biāo)識(shí)。返回Stage.RESOURCE_CACHE。如果為false,遞歸調(diào)用,判斷是否為diskCacheStrategy.decodeCachedData(),也就是指定磁盤緩存策略為DiskCacheStrategy.ALL或DiskCacheStrategy.DATA或由DiskCacheStrategy.AUTOMATIC對(duì)本地圖片使用磁盤緩存時(shí),此時(shí)返回true。否則返回false,遞歸調(diào)用,判斷onlyRetrieveFromCache的boolean值,這個(gè)值是初始化DecodeJob中傳進(jìn)來(lái)的,它代表是否僅從緩存加載圖片,通過(guò)onlyRetrieveFromCache(true)制定,默認(rèn)為false,如果為true,它意味著要從內(nèi)存或磁盤讀取,如果內(nèi)存或磁盤不存在該資源,則加載直接失敗。一般情況下我們不會(huì)制定,為false,也就是會(huì)返回 Stage.SOURCE。代表不使用磁盤緩存,也就是之前文章分析的,直接從服務(wù)器下載。關(guān)于onlyRetrieveFromCache,再多說(shuō)兩句:
某些情形下,你可能希望只要圖片不在緩存中則加載直接失?。ū热缡×髁磕J??–譯者注)。如果要完成這個(gè)目標(biāo),你可以在單個(gè)請(qǐng)求的基礎(chǔ)上使用 
GlideApp.with(fragment)
  .load(url)
  .onlyRetrieveFromCache(true)
  .into(imageView);
  • 所以,如果getNextStage方法返回的標(biāo)識(shí)為Stage.RESOURCE_CACHE或Stage.DATA_CACHE就代表我們沒(méi)有禁止磁盤緩存,那么willDecodeFromCache()將返回true。此時(shí)executor=diskCacheExecutor,返回false,executor=getActiveSourceExecutor();而這些executor在glide初始化的GlideBuilder.build方法里已經(jīng)被實(shí)例了。
  • 然后執(zhí)行executor.execute(decodeJob);接著會(huì)command.run();開(kāi)啟線程任務(wù),也就是執(zhí)行DecodeJob的run方法,run方法里調(diào)用runWrapped方法,runReason默認(rèn)為INITIALIZE
DecodeJob類
private void runWrapped() {
     switch (runReason) {
      case INITIALIZE:
        // 初始化 獲取下一個(gè)階段狀態(tài)
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        // 運(yùn)行
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }
  
private Stage getNextStage(Stage current) {
    switch (current) {
      case INITIALIZE: 
      // 根據(jù)定義的緩存策略來(lái)回去下一個(gè)狀態(tài)
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);
      case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);
      case DATA_CACHE:
        return Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }

// 根據(jù)Stage找到數(shù)據(jù)抓取生成器。
private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
       // 產(chǎn)生含有降低采樣/轉(zhuǎn)換資源數(shù)據(jù)緩存文件的DataFetcher。
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
       // 產(chǎn)生包含原始未修改的源數(shù)據(jù)緩存文件的DataFetcher。
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
      // 生成使用注冊(cè)的ModelLoader和加載時(shí)提供的Model獲取源數(shù)據(jù)規(guī)定的DataFetcher。
      // 根據(jù)不同的磁盤緩存策略,源數(shù)據(jù)可首先被寫入到磁盤,然后從緩存文件中加載,而不是直接返回。
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }
 private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    while (!isCancelled && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
    // We've run out of stages and generators, give up.
    if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
      notifyFailed();
    }

    // Otherwise a generator started a new load and we expect to be called back in
    // onDataFetcherReady.
  }
這里我們選擇ResourceCacheGenerator或DataCacheGenerator都好,我們就以ResourceGenerator為示例,在runGenerators()方法里,還是看currentGenerator.startNext()。
ResourceCacheGenerator類
@Override
  public boolean startNext() {
    List<Key> sourceIds = helper.getCacheKeys();
    if (sourceIds.isEmpty()) {
      return false;
    }
    List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
    while (modelLoaders == null || !hasNextModelLoader()) {
      resourceClassIndex++;
      if (resourceClassIndex >= resourceClasses.size()) {
        sourceIdIndex++;
        if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }
        resourceClassIndex = 0;
      }

      Key sourceId = sourceIds.get(sourceIdIndex);
      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
      Transformation<?> transformation = helper.getTransformation(resourceClass);

      currentKey = new ResourceCacheKey(sourceId, helper.getSignature(), helper.getWidth(),
          helper.getHeight(), transformation, resourceClass, helper.getOptions());
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
// 查找ModelLoader 
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(),
              helper.getOptions());
  // 通過(guò)FileLoader繼續(xù)加載數(shù)據(jù)
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }

    return started;
  }
FileLoad類
public void loadData(Priority priority, DataCallback<? super Data> callback) {
    // 讀取文件數(shù)據(jù)
     try {
       data = opener.open(file);
     } catch (FileNotFoundException e) {
       if (Log.isLoggable(TAG, Log.DEBUG)) {
         Log.d(TAG, "Failed to open file", e);
       }
    //失敗
       callback.onLoadFailed(e);
       return;
     }
  // 成功
     callback.onDataReady(data);
   }
在這里我們就可以看到根據(jù)key讀取緩存文件cacheFile,傳入File,得到對(duì)應(yīng)的modelloader.fetcher去獲取數(shù)據(jù),加載完畢后通過(guò),callback.onDataReady(result);把數(shù)據(jù)回調(diào)返回 此callback就是對(duì)應(yīng)的Generator,我們這里是指ResourceCacheGenerator
ResourceCacheGenerator類
 @Override
  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.RESOURCE_DISK_CACHE,
        currentKey);
  }
繼續(xù)回調(diào),cb為SourceGenerator
SourceGenerator類
  // Called from source cache generator.
  @Override
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
      DataSource dataSource, Key attemptedKey) {
    // This data fetcher will be loading from a File and provide the wrong data source, so override
    // with the data source of the original fetcher
    cb.onDataFetcherReady(sourceKey, data, fetcher, loadData.fetcher.getDataSource(), sourceKey);
  }

繼續(xù)回調(diào)cb為DecodeJob

DecodeJob類
  @Override
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
      DataSource dataSource, Key attemptedKey) {
    this.currentSourceKey = sourceKey;
    this.currentData = data;
    this.currentFetcher = fetcher;
    this.currentDataSource = dataSource;
    this.currentAttemptingKey = attemptedKey;
    if (Thread.currentThread() != currentThread) {
      runReason = RunReason.DECODE_DATA;
      callback.reschedule(this);
    } else {
      TraceCompat.beginSection("DecodeJob.decodeFromRetrievedData");
      try {
        decodeFromRetrievedData();
      } finally {
        TraceCompat.endSection();
      }
    }
  }
//然后判斷線程,這里原因上篇文章具體講解過(guò),最后還是執(zhí)行decodeFromRetrievedData();
  private void decodeFromRetrievedData() {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      logWithTimeAndKey("Retrieved data", startFetchTime,
          "data: " + currentData
          + ", cache key: " + currentSourceKey
          + ", fetcher: " + currentFetcher);
    }
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      e.setLoggingDetails(currentAttemptingKey, currentDataSource);
      throwables.add(e);
    }
    if (resource != null) {
      notifyEncodeAndRelease(resource, currentDataSource);
    } else {
      runGenerators();
    }
  }

在onDataReady方法回調(diào)給decodeJob的DataSource是DataSource.RESOURCE_DISK_CACHE
通過(guò)decodeFromData方法將數(shù)據(jù)解碼成Resource對(duì)象后返回即可。然后通過(guò)notifyEncodeAndRelease回調(diào)UI線程顯示出來(lái)。
至此,磁盤緩存的讀取邏輯完畢

【磁盤緩存的寫入】

我們?cè)赟ourceGenerator這個(gè)類的startNext方法觸發(fā)數(shù)據(jù)的加載時(shí), loadData.fetcher.loadData(helper.getPriority(), this);加載完畢會(huì)返調(diào)用SourceGenerator.onDataReady(result);將結(jié)果返回
SourceGenerator類
@Override
  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }
此時(shí)如果我們的磁盤緩存策略沒(méi)有禁止,那么 dataToCache = data;同時(shí)執(zhí)行 cb.reschedule();也就是DecodeJob.reschedule():
DecodeJob
public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
// We might be being called back on someone else's thread. Before doing anything, we should
    // reschedule to get back onto Glide's thread.

    callback.reschedule(this);
  }
callback.reschedule(this);也就是Engine.reschedule();再說(shuō)一遍原因是我們數(shù)據(jù)加載完被回調(diào)至此,我們可能在其他線程里,但是我們需要切換到Glide自定義的線程。
  @Override
  public void reschedule(DecodeJob<?> job) {
    // Even if the job is cancelled here, it still needs to be scheduled so that it can clean itself
    // up.
    getActiveSourceExecutor().execute(job);
  }
也就是GlideEexcutor的execute,在這里調(diào)用DecodeJob的run方法,--runWrapped,因?yàn)?runReason = RunReason.SWITCH_TO_SOURCE_SERVICE,所以直接調(diào)用 runGenerators();方法。--> 繼續(xù),currentGenerator.startNext()這里的代碼已經(jīng)重復(fù)很多很多次了,就不過(guò)多贅述了。
 @Override
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }
此時(shí)調(diào)用currentGenerator.startNext()方法dataToCache已經(jīng)不為null了。也就是cacheData(data);就是這里了,我們?cè)谶@里寫入數(shù)據(jù)。
private void cacheData(Object dataToCache) {
    long startTime = LogTime.getLogTime();
    try {
// 根據(jù)不同的數(shù)據(jù)獲取注冊(cè)的不同Encoder
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
//得到DiskCache得實(shí)現(xiàn),并存入磁盤。
      helper.getDiskCache().put(originalKey, writer);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished encoding source to cache"
            + ", key: " + originalKey
            + ", data: " + dataToCache
            + ", encoder: " + encoder
            + ", duration: " + LogTime.getElapsedMillis(startTime));
      }
    } finally {
      loadData.fetcher.cleanup();
    }
//
    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }

至此,磁盤緩存的寫入也講解完畢。

寫源碼分析真的十分頭疼 ? ? 給個(gè)小心心鼓勵(lì)一下吧~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 引言:2017年8月13日,因感印軍越邊近兩個(gè)月,心中激憤,夜不能寐,故作此詞明志! 夜來(lái)枕上心煩亂,坐起望空嘆。...
    李鴻鈞閱讀 643評(píng)論 1 9
  • 大雨滂沱之后, 我的心將更為澄凈。 若你肯等待, 所有漂浮不定的云彩, 都將化為幸福的海......。
    月白風(fēng)清L閱讀 172評(píng)論 0 2
  • 1 你不在身旁 風(fēng)都入了我的詩(shī)行 于是搗碎了整個(gè)寒塘 2 遇見(jiàn)你之后 我這漂泊踉蹌的孤舟 得意與失意并立船頭 3 ...
    不見(jiàn)得閱讀 357評(píng)論 0 1
  • 我聲稱你擁有了我的夢(mèng)大家都笑了起來(lái)笑吧 笑吧我憤怒地哭著你怎么還不來(lái) 明明我是唯一的清醒智者怎么他們一笑倒成了跳梁...
    長(zhǎng)舌婦閱讀 317評(píng)論 2 1

友情鏈接更多精彩內(nèi)容