Glide 系列(四) Glide緩存機制

上篇我們以加載一張網(wǎng)絡(luò)圖片為例嗽上,講解了Glide加載一張圖片的整體流程。為了更連貫的理解流程我們略過了一些細節(jié)伊约,包括緩存功能姚淆,本篇我們來講解Glide的二級緩存機制。
緩存流程是穿插在Glide整體加載流程中的屡律,所以建議讀這篇為文章之前先了解上篇文章《Glide 系列(三) Glide源碼整體流程梳理》腌逢。
由于Glide的代碼復雜,我們先回顧下Glide緩存相關(guān)的用法超埋,和整體上回顧下Glide的加載流程的主要類搏讶。

Glide緩存功能相關(guān)用法

設(shè)置內(nèi)存緩存開關(guān):

skipMemoryCache(true)

設(shè)置磁盤緩存模式:

diskCacheStrategy(DiskCacheStrategy.NONE)

可以設(shè)置4種模式:

  • DiskCacheStrategy.NONE:表示不緩存任何內(nèi)容佳鳖。
  • DiskCacheStrategy.SOURCE:表示只緩存原始圖片。
  • DiskCacheStrategy.RESULT:表示只緩存轉(zhuǎn)換過后的圖片(默認選項)媒惕。
  • DiskCacheStrategy.ALL :表示既緩存原始圖片系吩,也緩存轉(zhuǎn)換過后的圖片。

Glide加載圖片主要類

這里只簡單介紹主干流程吓笙,方便介紹緩存功能是定位源碼淑玫,詳細流程請移步上篇文章。
首先我們想下一個圖片框架面睛,應該包含哪幾個模塊:

功能模塊.png
  • 對外接口:封裝該框架的功能接口絮蒿,一般為單例模式。
  • 獲取圖片Request:為每個圖片加載創(chuàng)建一個Request叁鉴,用來準備數(shù)據(jù)土涝、請求圖片資源。
  • 異步處理:不管從網(wǎng)絡(luò)還是從本地讀取圖片都是耗時操作幌墓,需要在子線程中完成但壮。
  • 網(wǎng)絡(luò)連接 :網(wǎng)絡(luò)獲取圖片的必備模塊。
  • 解碼 :獲得圖片流后要解碼得到圖片對象常侣。

上面幾部分是圖片框架所必備的模塊蜡饵,Glide也不例外,接下來看看Glide各模塊對應的主要類:

Glide功能模塊主要類.png
  • Glide:Glide除了是接口封裝類胳施,還負責創(chuàng)建全局使用的工具和組件溯祸。
  • GenericRequest:為每個圖片加載創(chuàng)建一個Request,初始化這張圖片的轉(zhuǎn)碼器舞肆、圖片變換器焦辅、圖片展示器target等,當然這個過程實在GenericRequestBuilder的實現(xiàn)類里完成的椿胯。
  • Engine:異步處理總調(diào)度器筷登。EnginJob負責線程管理,EngineRunnable是一個異步處理線程哩盲。DecodeJob是真正線程里獲取和處理圖片的地方前方。
  • HttpUrlFetcher :獲取網(wǎng)絡(luò)流,使用的是HttpURLConnection种冬。
  • Decoder :讀取網(wǎng)絡(luò)流后镣丑,解碼得到Bitmap或者gifResource。因為加載圖片類型不同娱两,這快分支較多,學習Glide初級階段金吗,這塊可以先不再詳細分析十兢。
    接下來我們進入主題趣竣,緩存的代碼在上面流程圖里的什么位置?內(nèi)存緩存的操作應該是在異步處理之前旱物,磁盤緩存是耗時操作應該是在異步處理中完成遥缕。

Glide內(nèi)存緩存源碼分析

內(nèi)存存緩存的 讀存都在Engine類中完成。

Glide內(nèi)存緩存的特點

內(nèi)存緩存使用弱引用和LruCache結(jié)合完成的,弱引用來緩存的是正在使用中的圖片宵呛。圖片封裝類Resources內(nèi)部有個計數(shù)器判斷是該圖片否正在使用单匣。

Glide內(nèi)存緩存的流程
  • 讀:是先從lruCache取,取不到再從弱引用中缺λ搿户秤;
  • 存:內(nèi)存緩存取不到,從網(wǎng)絡(luò)拉取回來先放在弱引用里逮矛,渲染圖片鸡号,圖片對象Resources使用計數(shù)加一;
  • 渲染完圖片须鼎,圖片對象Resources使用計數(shù)減一鲸伴,如果計數(shù)為0,圖片緩存從弱引用中刪除晋控,放入lruCache緩存汞窗。

具體看源碼:
上篇提到,Engine在加載流程的中的入口方法是load方法:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId();
        //生成緩存的key
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());
        //從LruCache獲取緩存圖片
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }
        //從弱引用獲取圖片
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }

        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);
        }

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

    ...
}

上面是從內(nèi)存緩存中讀取圖片的主流程:

  • 生成緩存的key赡译。
  • 從LruCache獲取緩存圖片仲吏。
  • LruCache沒取到,從弱引用獲取圖片捶朵。
  • 內(nèi)存緩存取不到蜘矢,進入異步處理。

我們具體看取圖片的兩個方法loadFromCache()和loadFromActiveResources()综看。loadFromCache使用的就是LruCache算法品腹,loadFromActiveResources使用的就是弱引用。我們來看一下它們的源碼:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    private final MemoryCache cache;
    private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
    ...

    private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> cached = getEngineResourceFromCache(key);
        if (cached != null) {
            cached.acquire();
            activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
        }
        return cached;
    }

    private EngineResource<?> getEngineResourceFromCache(Key key) {
        Resource<?> cached = cache.remove(key);
        final EngineResource result;
        if (cached == null) {
            result = null;
        } else if (cached instanceof EngineResource) {
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true /*isCacheable*/);
        }
        return result;
    }

    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();
            if (active != null) {
                active.acquire();
            } else {
                activeResources.remove(key);
            }
        }
        return active;
    }

    ...
}

loadFromCache()方法:

  • 首先就判斷isMemoryCacheable是不是false红碑,如果是false的話就直接返回null舞吭。這就是skipMemoryCache()方法設(shè)置的是否內(nèi)存緩存已被禁用。
  • 然后調(diào)用getEngineResourceFromCache()方法來獲取緩存析珊。在這個方法中羡鸥,會從中獲取圖片緩存LruResourceCache,LruResourceCache其實使用的就是LruCache算法實現(xiàn)的緩存忠寻。
  • 當我們從LruResourceCache中獲取到緩存圖片之后會將它從緩存中移除惧浴,將緩存圖片存儲到activeResources當中。activeResources就是弱引用的HashMap奕剃,用來緩存正在使用中的圖片衷旅。

loadFromActiveResources()方法:

  • 就是從activeResources這個activeResources當中取值的捐腿。使用activeResources來緩存正在使用中的圖片,用來保護正在使用中的圖片不會被LruCache算法回收掉柿顶。

這樣我們把從內(nèi)存讀取圖片緩存的流程搞清了茄袖,那是什么時候存儲的呢。想想什么時候合適嘁锯?是不是應該在異步處理獲取到圖片后宪祥,再緩存到內(nèi)存?
參考上一篇家乘,EngineJob 獲取到圖片后 會回調(diào)Engine的onEngineJobComplete()蝗羊。我們來看下做了什么:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    @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()));
            }
        }
        jobs.remove(key);
    }

    ...
}

在onEngineJobComplete()方法里將正在加載的圖片放到弱引用緩存。那什么時候放在LruCache里呢烤低?當然是在使用完肘交,那什么時候使用完呢?

那我們來看EngineResource這個類是怎么標記自己是否在被使用的扑馁。EngineResource是用一個acquired變量用來記錄圖片被引用的次數(shù)涯呻,調(diào)用acquire()方法會讓變量加1,調(diào)用release()方法會讓變量減1腻要,代碼如下所示:

class EngineResource<Z> implements Resource<Z> {

    private int acquired;
    ...

    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);
        }
    }
}

可以看出當引用計數(shù)acquired變量為0复罐,就是沒有在使用了,然后調(diào)用了 listener.onResourceReleased(key, this);
這個listener就是Engine對象雄家,我們來看下它的onResourceReleased()方法:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    private final MemoryCache cache;
    private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
    ...    

    @Override
    public void onResourceReleased(Key cacheKey, EngineResource resource) {
        Util.assertMainThread();
        activeResources.remove(cacheKey);
        if (resource.isCacheable()) {
            cache.put(cacheKey, resource);
        } else {
            resourceRecycler.recycle(resource);
        }
    }

    ...
}

做了三件事:

  • 從弱引用刪除圖片緩存
  • 是否支持緩存效诅,緩存到LruCache緩存
  • 不支持緩存直接調(diào)用垃圾回收,回收圖片

到這里內(nèi)存緩存的讀和存的流程就介紹完了趟济,根據(jù)源碼回頭看看我們之前列的Glide內(nèi)存緩存流程乱投,就清晰很多了。

Glide磁盤緩存源碼分析

Glide磁盤緩存流程

先列下主流程顷编,再具體看代碼

  • 讀:先找處理后(result)的圖片戚炫,沒有的話再找原圖。
  • 存:先存原圖媳纬,再存處理后的圖双肤。

注意一點:diskCacheStrategy設(shè)置的的緩存模式即影響讀取,也影響存儲钮惠。

具體看源碼:
源碼入口位置是在EngineRunnable的run()方法茅糜,run()方法中調(diào)用到decode()方法,decode()方法的源碼:

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        //從磁盤緩存讀取圖片
        return decodeFromCache();
    } else { 
        //從原始位置讀取圖片
        return decodeFromSource();
    }
}

來看一下decodeFromCache()方法的源碼素挽,如下所示:

private Resource<?> decodeFromCache() throws Exception {
    Resource<?> result = null;
    try {
        //先嘗試讀取處理后的緩存圖
        result = decodeJob.decodeResultFromCache();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Exception decoding result from cache: " + e);
        }
    }
    if (result == null) {
       //再嘗試讀取原圖的緩存圖
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}

處理后的緩存圖和原圖緩存圖對應的是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE這兩個緩存模式蔑赘。
到DecodeJob具體看下這兩個讀取磁盤緩存的方法,decodeResultFromCache()和decodeSourceFromCache():

public Resource<Z> decodeResultFromCache() throws Exception {
    if (!diskCacheStrategy.cacheResult()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = loadFromCache(resultKey);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

public Resource<Z> decodeSourceFromCache() throws Exception {
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
    return transformEncodeAndTranscode(decoded);
}

這里兩個方法都先判斷了是否是對應的緩存模式,不是則讀取失敗米死。這里我們不關(guān)注transform 和transcode的相關(guān)功能锌历,只分析緩存功能贮庞。兩個緩存方法都調(diào)用到了loadFromCache()方法峦筒,只是傳入的key不同。一個是處理后圖片的key窗慎,一個是原始圖片的key物喷。
繼續(xù)看loadFromCache()方法的源碼:

private Resource<T> loadFromCache(Key key) throws IOException {
    File cacheFile = diskCacheProvider.getDiskCache().get(key);
    if (cacheFile == null) {
        return null;
    }
    Resource<T> result = null;
    try {
        result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
    } finally {
        if (result == null) {
            diskCacheProvider.getDiskCache().delete(key);
        }
    }
    return result;
}

源碼中可以看到我們是從diskCacheProvider.getDiskCache()中讀取的緩存,diskCacheProvider.getDiskCache()獲得的是DiskLruCache工具類的實例遮斥,然后從DiskLruCache獲取緩存峦失。之后的decode不是本篇的關(guān)注點,先不分析术吗。
到這里我們把從磁盤緩存讀取緩存的流程講完了尉辑,那什么時候存入的呢?肯定是在從原始位置獲取圖片后较屿,我們回到decodeFromSource()方法隧魄,一步步看進去:

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}

decodeSource()顧名思義是用來解析原圖片的,而transformEncodeAndTranscode()則是用來對圖片進行轉(zhuǎn)換和轉(zhuǎn)碼的隘蝎。我們先來看decodeSource()方法:

private Resource<T> decodeSource() throws Exception {
    Resource<T> decoded = null;
    try {
        long startTime = LogTime.getLogTime();
        //從網(wǎng)絡(luò)獲取圖片
        final A data = fetcher.loadData(priority);
        if (isCancelled) {
            return null;
        }
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}

private Resource<T> decodeFromSourceData(A data) throws IOException {
    final Resource<T> decoded;
    //判斷設(shè)置了是否緩存原圖
    if (diskCacheStrategy.cacheSource()) {
        decoded = cacheAndDecodeSourceData(data);
    } else {
        long startTime = LogTime.getLogTime();
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
    }
    return decoded;
}

private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
    long startTime = LogTime.getLogTime();
    SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
    diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
    startTime = LogTime.getLogTime();
    Resource<T> result = loadFromCache(resultKey.getOriginalKey());
    return result;
}

decodeSource()方法中獲取圖片后购啄,調(diào)用到decodeFromSourceData()方法,然后判斷是否緩存原圖嘱么,是的話就調(diào)用到cacheAndDecodeSourceData(A data)方法狮含。看進去曼振,還是調(diào)用了 diskCacheProvider.getDiskCache()獲取DiskLruCache工具類的實例几迄。然后調(diào)用put方法緩存了原圖。

到此我們緩存了原圖冰评,處理后的圖片是什么時候緩存的映胁?肯定是在圖片處理之后,在transformEncodeAndTranscode()方法中:

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = transform(decoded);
    writeTransformedToCache(transformed);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

private void writeTransformedToCache(Resource<T> transformed) {
    if (transformed == null || !diskCacheStrategy.cacheResult()) {
        return;
    }
    long startTime = LogTime.getLogTime();
    SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
    diskCacheProvider.getDiskCache().put(resultKey, writer);
}

transformEncodeAndTranscode中先對圖片進行了轉(zhuǎn)換集索,然后調(diào)用writeTransformedToCache(transformed);判斷是否緩存處理后的圖片屿愚,是就對處理后的圖片進行了緩存。調(diào)用的同樣是DiskLruCache實例的put()方法务荆,不過這里用的緩存Key是resultKey妆距。
至此圖片磁盤緩存都講解完了,對照源碼看下之前的Glide磁盤緩存流程是不是清晰了很多函匕。
本篇我們主要講解了Glide的二級緩存機制娱据,雖然代碼比較長,但是基本流程比較清晰盅惜,大家通過對流程的梳理中剩,加深對Glide緩存機制的理解忌穿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市结啼,隨后出現(xiàn)的幾起案子掠剑,更是在濱河造成了極大的恐慌,老刑警劉巖郊愧,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朴译,死亡現(xiàn)場離奇詭異,居然都是意外死亡属铁,警方通過查閱死者的電腦和手機眠寿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焦蘑,“玉大人盯拱,你說我怎么就攤上這事±觯” “怎么了狡逢?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蝶防。 經(jīng)常有香客問我甚侣,道長,這世上最難降的妖魔是什么间学? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任殷费,我火速辦了婚禮,結(jié)果婚禮上低葫,老公的妹妹穿的比我還像新娘详羡。我一直安慰自己,他們只是感情好嘿悬,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布实柠。 她就那樣靜靜地躺著,像睡著了一般善涨。 火紅的嫁衣襯著肌膚如雪窒盐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天钢拧,我揣著相機與錄音蟹漓,去河邊找鬼。 笑死源内,一個胖子當著我的面吹牛葡粒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼嗽交,長吁一口氣:“原來是場噩夢啊……” “哼卿嘲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起夫壁,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拾枣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后掌唾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體放前,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年糯彬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葱她。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡撩扒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吨些,到底是詐尸還是另有隱情搓谆,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布豪墅,位于F島的核電站泉手,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏偶器。R本人自食惡果不足惜斩萌,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望屏轰。 院中可真熱鬧颊郎,春花似錦、人聲如沸霎苗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽唁盏。三九已至内狸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厘擂,已是汗流浹背昆淡。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驴党,地道東北人瘪撇。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親倔既。 傳聞我的和親對象是個殘疾皇子恕曲,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

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

  • Android緩存機制:如果沒有緩存,在大量的網(wǎng)絡(luò)請求從遠程獲取圖片時會造成網(wǎng)絡(luò)流量的浪費渤涌,加載速度較慢佩谣,用戶體驗...
    芒果味的你呀閱讀 4,462評論 13 22
  • 學習來源:郭霖大師博客地址 1、圖片加載框架挺多实蓬,如Volley茸俭、Glide、Picasso安皱、Fresco调鬓、本次是...
    子謙寶寶閱讀 1,759評論 0 6
  • 7.1 壓縮圖片 一、基礎(chǔ)知識 1酌伊、圖片的格式 jpg:最常見的圖片格式腾窝。色彩還原度比較好,可以支持適當壓縮后保持...
    AndroidMaster閱讀 2,522評論 0 13
  • 小編做頭條也有也有一段時間了居砖,根據(jù)以往發(fā)表的文章總結(jié)下來發(fā)現(xiàn)虹脯,想要寫出爆文,標題最關(guān)鍵奏候。而想寫出今日頭條爆文爆文標...
    無比簡單閱讀 975評論 0 0
  • 今天上語文課了時候循集,戴老師讓我坐到老師下邊聽講,我聽會了之后蔗草,戴老師就讓我當小老師了咒彤。當小老師的時候,我覺得很開...
    小狐貍的麻麻閱讀 277評論 0 2