源碼分析--Glide源碼 三 Glide的緩存

寫在前面

對於一個應用來講所需要的圖片不可能總是來自他自己的apk包中,總會有一些實時的圖片來自於網絡、服務器中,而爲了流量、加載速度等方面的考慮,我們做不到每一次都是從網絡中下載,爲了解決這個問題,我們提出了緩存這個概念。
如果你發現本文中有任何錯誤,請在評論區留言或者私信我,我會第一時間改正,謝謝!

0 圖片的三級緩存

對於圖片來講,緩存是十分有必要的,在Android的發展史中,圖片的緩存慢慢分成了三級。
內存緩存 本地緩存 網絡
相對具體的可以參看這篇文章:三級緩存

1 Glide中的disk緩存

爲什麼先要將disk緩存,而不是內存緩存呢?
因爲disk緩存,是緊挨着網絡的,而內存緩存不是緊挨網絡的,內存緩存與網絡中間還隔了一層disk緩存。由於已經初步瞭解Glide的加載過程之後,先去了解disk緩存遠遠比了解內存緩存要容易。
在Glide中,disk緩存的獲取是在EngineRunning中,在上一篇博文中,我們可以知道第一次執行EngineRunning的狀態都是Stage.CACHE,即從緩存中獲取,看一下具體的實現

//EngineRunning
private Resource<?> decode() throws Exception {
    //不設置的話這個判斷第一次是會放回true
    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;
}

//decodeJob.java
/**
 * Returns a transcoded resource decoded from transformed resource data in the disk cache, or null if no such
 * resource exists.
 *
 * @throws Exception
 */
public Resource<Z> decodeResultFromCache() throws Exception {
    if (!diskCacheStrategy.cacheResult()) {
        return null;
    }

    long startTime = LogTime.getLogTime();
    Resource<T> transformed = loadFromCache(resultKey);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Decoded transformed from cache", startTime);
    }
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Transcoded transformed from cache", startTime);
    }
    return result;
}

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

不難看出這一層是使用diskLruCache來獲取緩存的,再看下去

//EngineRunnable.java
private Resource<?> decodeFromCache() throws Exception {
    ...
    if (result == null) {
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}

//DecodeJob.java
public Resource<Z> decodeSourceFromCache() throws Exception {
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }

    long startTime = LogTime.getLogTime();
    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Decoded source from cache", startTime);
    }
    return transformEncodeAndTranscode(decoded);
}

乍一看,這更前面獲取disk緩存沒什麼差別,但是其實這裏是做了小區分的。
在Glide中,對應緩存key的類是EngineKey,而這個EngineKey的構成是由很多因素構成的,至於會被什麼因素影響,可以通過EngineKey的構造函數粗略的知曉。
而在這裏,這兩個獲取disk緩存的key是不同地,第一次獲取用的是完整的EngineKey,即是帶有長、寬等約束條件的EngineKey,獲取出來之後可以直接複用。
而第二次獲取用的key帶的約束條件只有id和signature,即沒有加工過的原圖片。如果這個原圖片可以獲取得到的話,需要後續加工即transformEncodeAndTranscode方法後,才能使用。
再看一下transformEncodeAndTranscode的具體實現

 private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
        long startTime = LogTime.getLogTime();
        //根據長寬先加工一遍圖片
        Resource<T> transformed = transform(decoded);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Transformed resource from source", startTime);
        }
		
        writeTransformedToCache(transformed);

        startTime = LogTime.getLogTime();
        Resource<Z> result = transcode(transformed);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Transcoded transformed from source", startTime);
        }
        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);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Wrote transformed from source to cache", startTime);
        }
    }

可以看到writeTransformedToCache又把圖片資源文件(帶有長寬的)放入disk緩存中,至此disk緩存中就有兩張照片了,他們一張是原圖,一張是加工後的圖片。
這以上都是取得邏輯,再看一下存的邏輯,當下載完成圖片之後,會把圖片存到disk緩存中,邏輯還在decodeJob類中,關注一下

//decodeJob
private Resource<T> decodeSource() throws Exception {
        Resource<T> decoded = null;
        try {
            long startTime = LogTime.getLogTime();
            final A data = fetcher.loadData(priority);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Fetched data", startTime);
            }
            if (isCancelled) {
                return null;
            }
            decoded = decodeFromSourceData(data);
        } finally {
            fetcher.cleanup();
        }
        return decoded;
    }

private Resource<T> decodeFromSourceData(A data) throws IOException {
        final Resource<T> decoded;
        //判斷是否要disk緩存
        if (diskCacheStrategy.cacheSource()) {
            decoded = cacheAndDecodeSourceData(data);
        } else {
            long startTime = LogTime.getLogTime();
            decoded = loadProvider.getSourceDecoder().decode(data, width, height);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Decoded from source", startTime);
            }
        }
        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);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Wrote source to cache", startTime);
        }

        startTime = LogTime.getLogTime();
        Resource<T> result = loadFromCache(resultKey.getOriginalKey());
        if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {
            logWithTimeAndKey("Decoded source from cache", startTime);
        }
        return result;
    }

下載好的圖片首先會經過decodeFromSourceData方法,然後進入cacheAndDecodeSourceData這個方法是存原圖的。後續的話就會過一遍transformEncodeAndTranscode這個方法(存帶有長寬的圖片)
到這裏disk緩存也就講完了。

2 Glide中的內存緩存

相比於disk緩存,內存緩存的相應速度要快上許多,我們最期望的圖片獲取方式是從內存中獲取,看一下Glide是怎麼獲取的吧
在Glide中,內存獲取的方式是在Engine中,看一下具體的實現

 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();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

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

可以看到這裏先創建了一個EngineKey,然後再調用loadFromCache方法,看一下這個方法的實現

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) {
            // Save an object allocation if we've cached an EngineResource (the typical case).
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true /*isCacheable*/);
        }
        return result;
    }

這裏先判斷是否需要使用內存緩存,不需要直接return null,接着調用了getEngineResourceFromCache這個方法。getEngineResourceFromCache這個方法嘗試從cache中獲取圖片,cache是在with過程中就被初始話的參數

//GlideBuilder.java
    	if (memoryCache == null) {
            memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
        }

        if (diskCacheFactory == null) {
            diskCacheFactory = new InternalCacheDiskCacheFactory(context);
        }

看一下這個LruResourceCache類,它是繼承了LruCache的,顯然他就是屬於內存緩存的。那麼getEngineResourceFromCache的作用就是從lru中獲取出來對應的圖片並吧圖片轉化成EngineResource類。回到loadFromCache方法,得到圖片之後他做了一個很有意思的操作。

  if (cached != null) {
            cached.acquire();
            activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
        }

看一下cached.acquire()這個的實現

//EngineResource.java
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);
        }
    }

這個方法將EngineResource的acquired+1,與之對應的是在release的時候-1,還可以看到release的時候還多了一個判斷,並且當acquired==0的時候調用了 listener.onResourceReleased方法。這個listener我們先暫時放一下,這裏還不能確定他指的是什麼。
cached.acquire();之後,他還調用了activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));方法,activeResources是一個HashMap<Key, WeakReference<EngineResource<?>>>()。用於存儲從memoryCache中remove出來的值。再回到load方法裏面

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

如果從loadFromCache從LruCache中或取出來圖片,直接通過回調返回出去。再看下去

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

又調用了loadFromActiveResources方法獲取,看一下具體的實現

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

這個實現也非常的簡單,就是在我們剛剛介紹過activeResources中通過key查找緩存,如果有就返回,沒有就返回null
看完了獲取內存緩存,再看一下怎麼放到內存緩存中的。根據圖片的三級緩存規則來講,放到內存緩存應該是從網絡下載完圖片之後,在順便放到disk和內存中的。具體的代碼是從EngineJob中開始的

private void handleResultOnMainThread() {
        if (isCancelled) {
            resource.recycle();
            return;
        } else if (cbs.isEmpty()) {··········
            throw new IllegalStateException("Received a resource without any callbacks to notify");
        }
        //關注以下部分代碼
        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);
            }
        }
        // Our request is complete, so we can release the resource.
        engineResource.release();
    }

首先,根據資源創建相應的EngineResource,然後調用了我們之前調用的acquire方法,注意一下,每有一個callback都會調用一次acquire.再看一下listener.onEngineJobComplete(key, engineResource)方法

//Engine.java
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);
    }

這個方法裏給resource設置了Listener。那麼看一下這個listener幹了什麼吧

//EngineResource.java
    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);
        }
    }

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

這個Listener在acquired =0的時候,會把activeResources中的圖片放回到LruCache中。
結合以上的總結,可以知道acquired是用來表示這個圖片資源有多少正在被使用的。當這個值被清空的時候,會把弱引用中的圖片放回LruCache中。
總的來講,Glide緩存相關的邏輯也就講完了。這裏整個緩存的邏輯是很完善,非要雞蛋裏挑骨頭的話,只有在內存緩存中獲取的邏輯順序應該調換一下,應該先從hashmap中獲取,然後在從LruCache中獲取,而在Glide的後續版本(4.0.10)上面,這裏也確實被調整過來了。

/**
     * Starts a load for the given arguments.
     *
     * <p>Must be called on the main thread.
     *
     * <p>The flow for any request is as follows:
     *
     * <ul>
     * <li>Check the current set of actively used resources, return the active resource if present,
     * and move any newly inactive resources into the memory cache.
     * <li>Check the memory cache and provide the cached resource if present.
     * <li>Check the current set of in progress loads and add the cb to the in progress load if one
     * is present.
     * <li>Start a new load.
     * </ul>
     *
     * <p>Active resources are those that have been provided to at least one request and have not yet
     * been released. Once all consumers of a resource have released that resource, the resource then
     * goes to cache. If the resource is ever returned to a new consumer from cache, it is re-added to
     * the active resources. If the resource is evicted from the cache, its resources are recycled and
     * re-used if possible and the resource is discarded. There is no strict requirement that
     * consumers release their resources so active resources are held weakly.
     *
     * @param width  The target width in pixels of the desired resource.
     * @param height The target height in pixels of the desired resource.
     * @param cb     The callback that will be called when the load completes.
     */
    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,
            Executor callbackExecutor) {
        long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

        EngineKey key =
                keyFactory.buildKey(
                        model,
                        signature,
                        width,
                        height,
                        transformations,
                        resourceClass,
                        transcodeClass,
                        options);

        EngineResource<?> memoryResource;
        synchronized (this) {
            memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

            if (memoryResource == null) {
                return waitForExistingOrStartNewJob(
                        glideContext,
                        model,
                        signature,
                        width,
                        height,
                        resourceClass,
                        transcodeClass,
                        priority,
                        diskCacheStrategy,
                        transformations,
                        isTransformationRequired,
                        isScaleOnlyOrNoTransform,
                        options,
                        isMemoryCacheable,
                        useUnlimitedSourceExecutorPool,
                        useAnimationPool,
                        onlyRetrieveFromCache,
                        cb,
                        callbackExecutor,
                        key,
                        startTime);
            }
        }

        // Avoid calling back while holding the engine lock, doing so makes it easier for callers to
        // deadlock.
        cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
        return null;
    }
@Nullable
    private EngineResource<?> loadFromMemory(
            EngineKey key, boolean isMemoryCacheable, long startTime) {
        if (!isMemoryCacheable) {
            return null;
        }

        EngineResource<?> active = loadFromActiveResources(key);
        if (active != null) {
            if (VERBOSE_IS_LOGGABLE) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return active;
        }

        EngineResource<?> cached = loadFromCache(key);
        if (cached != null) {
            if (VERBOSE_IS_LOGGABLE) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return cached;
        }

        return null;
    }

3 緩存總結

1.Glide的緩存是從Engine開始的,首先生成一個EngineKey,從內存中獲取,內存緩存分兩類,一種是hashmap保存的弱引用的圖片,這種圖片是當前頁面正在顯示的圖片,一種是lruCache緩存,保存着從hashmap中移除的照片。
2.如果需要的圖片內存緩存中都沒有,那麼會新建一個EngineJob,然後執行EngineRunning從disk緩存中獲取,第一次也是根據EngineKey去disk緩存中查找符合要求的圖片,如果有返回,執行3步驟。如果沒有,則再去尋找是否有這張圖原圖(即沒有處理過寬高的圖片),如果有,執行3步驟。如果沒有,再去發起網絡請求,將得到的圖片的原圖保存下來,在執行3步驟。
3.處理中這張圖片的寬高,在執行3步驟
4.存入hashmap中,標記爲再用圖片
5.hashmap中的圖片沒有被引用的對象,則進入lruCache中

4.Glide into完整過程

那麼我們就可以描述完整的一次Glide流程
首先,先會處理ImageView的ScaleType,然後把ImageView處理成一個Target,接着看這個target是否有前一個圖片加載請求,如果有就標記無效掉他並清空狀態,如果沒有就創建出一個request並給這個request創建回調及監聽(with中創建的空白的fragment),然後通過requestTracker執行這個request。
緊接着,會測量出target的長寬並回調onSizeReady方法,在這個同時會設置佔位圖。onSizeReady方法裏面,除了賦值長寬之外,還會啓動Engine的load方法,在load中首先會獲取到一個EngineKey,這個key是由長,寬,地址等構成。組件完成之後,先從兩級內存緩存中查找是否有所需要的圖片,如果有直接返回。然後會查看是否有這個EngineKey的EngineJob在執行,如果有的話,就不再請求了,採用addCallBack回調集合的方式完成圖片的下載。如果沒有的話,就通過EngineJobFactory創建一個新的EngineJob,同時創建decodeJob和EngineRunnable,然後通過EngineJob來執行EngineRunning。
首先執行的從disk緩存中獲取圖片,先從disk緩存中,先獲取帶有長寬限制的圖片,再獲取原圖,如果獲取不到,執行從Source中獲取圖片的方法。這個方法就是請求網絡的方法,是由之前在load方法中創建的SteamFetcher來作爲下載圖片的工具,這個SteamFetcher是由load的對象決定,返回的對象是InputSteam。
下載完成獲取到InputSteam之後,decodeJob把InputSteam轉化成Resource對象並存下原圖與有長寬限制的圖片。接着調用EngineRunning的onLoadComplete方法,onLoadComplete會回調EngineJob的onResourceReady方法,onResourceReady會向EngineJob中的handler發送一條消息,並切換到主線程。切換到主線程之後,EngineJob會回調Engine的方法,設置資源爲內存緩存,EngineJob又會回調之前callback集合,通知圖片已經準備完成,這個callback實現實在GenericRequest中。接下來,EngineJob中的callback會調用target.onResourceReady方法,這個方法最後也會調用到target.setResource方法,把獲取到的圖片設置到對應的ImagView中。

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