深入剖析Android的Volley庫(kù)中的圖片加載功能
一、基本使用要點(diǎn)回顧
Volley框架在請(qǐng)求網(wǎng)絡(luò)圖片方面也做了很多工作,提供了好幾種方法.本文介紹使用ImageLoader來(lái)進(jìn)行網(wǎng)絡(luò)圖片的加載.
ImageLoader的內(nèi)部使用ImageRequest來(lái)實(shí)現(xiàn),它的構(gòu)造器可以傳入一個(gè)ImageCache緩存形參,實(shí)現(xiàn)了圖片緩存的功能,同時(shí)還可以過(guò)濾重復(fù)鏈接,避免重復(fù)發(fā)送請(qǐng)求。
下面是ImageLoader加載圖片的實(shí)現(xiàn)方法:
public void displayImg(View view){ ImageView imageView = (ImageView)this.findViewById(R.id.image_view); RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext()); ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache()); ImageListener listener = ImageLoader.getImageListener(imageView,R.drawable.default_image, R.drawable.default_image); imageLoader.get("http://developer.android.com/images/home/aw_dac.png", listener); //指定圖片允許的最大寬度和高度 //imageLoader.get("http://developer.android.com/images/home/aw_dac.png",listener, 200, 200); }
使用ImageLoader.getImageListener()方法創(chuàng)建一個(gè)ImageListener實(shí)例后,在imageLoader.get()方法中加入此監(jiān)聽(tīng)器和圖片的url,即可加載網(wǎng)絡(luò)圖片.
下面是使用LruCache實(shí)現(xiàn)的緩存類
public class BitmapCache implements ImageCache { private LruCache<String, Bitmap> cache; public BitmapCache() { cache = new LruCache<String, Bitmap>(8 * 1024 * 1024) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight(); } }; } @Override public Bitmap getBitmap(String url) { return cache.get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { cache.put(url, bitmap); } }
最后,別忘記在AndroidManifest.xml文件中加入訪問(wèn)網(wǎng)絡(luò)的權(quán)限
<uses-permission android:name="android.permission.INTERNET"/>
二、源碼分析
(一) 初始化Volley請(qǐng)求隊(duì)列
mReqQueue = Volley.newRequestQueue(mCtx);
主要就是這一行了:
#Volley public static RequestQueue newRequestQueue(Context context) { return newRequestQueue(context, null); } public static RequestQueue newRequestQueue(Context context, HttpStack stack) { return newRequestQueue(context, stack, -1); } public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) { File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR); String userAgent = "volley/0"; try { String packageName = context.getPackageName(); PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); userAgent = packageName + "/" + info.versionCode; } catch (NameNotFoundException e) { } if (stack == null) { if (Build.VERSION.SDK_INT >= 9) { stack = new HurlStack(); } else { // Prior to Gingerbread, HttpUrlConnection was unreliable. // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); } } Network network = new BasicNetwork(stack); RequestQueue queue; if (maxDiskCacheBytes <= -1) { // No maximum size specified queue = new RequestQueue(new DiskBasedCache(cacheDir), network); } else { // Disk cache size specified queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network); } queue.start(); return queue; }
這里主要就是初始化HttpStack,對(duì)于HttpStack在API大于等于9的時(shí)候選擇HttpUrlConnetcion,反之則選擇HttpClient,這里我們并不關(guān)注Http相關(guān)代碼。
接下來(lái)初始化了RequestQueue,然后調(diào)用了start()方法。
接下來(lái)看RequestQueue的構(gòu)造:
public RequestQueue(Cache cache, Network network) { this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE); } public RequestQueue(Cache cache, Network network, int threadPoolSize) { this(cache, network, threadPoolSize, new ExecutorDelivery(new Handler(Looper.getMainLooper()))); } public RequestQueue(Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) { mCache = cache; mNetwork = network; mDispatchers = new NetworkDispatcher[threadPoolSize]; mDelivery = delivery; }
初始化主要就是4個(gè)參數(shù):mCache、mNetwork、mDispatchers、mDelivery。第一個(gè)是硬盤(pán)緩存;第二個(gè)主要用于Http相關(guān)操作;第三個(gè)用于轉(zhuǎn)發(fā)請(qǐng)求的;第四個(gè)參數(shù)用于把結(jié)果轉(zhuǎn)發(fā)到UI線程(ps:你可以看到new Handler(Looper.getMainLooper()))。
接下來(lái)看start方法
#RequestQueue /** * Starts the dispatchers in this queue. */ public void start() { stop(); // Make sure any currently running dispatchers are stopped. // Create the cache dispatcher and start it. mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); mCacheDispatcher.start(); // Create network dispatchers (and corresponding threads) up to the pool size. for (int i = 0; i < mDispatchers.length; i++) { NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); mDispatchers[i] = networkDispatcher; networkDispatcher.start(); } }
首先是stop,確保轉(zhuǎn)發(fā)器退出,其實(shí)就是內(nèi)部的幾個(gè)線程退出,這里大家如果有興趣可以看眼源碼,參考下Volley中是怎么處理線程退出的(幾個(gè)線程都是while(true){//doSomething})。
接下來(lái)初始化CacheDispatcher,然后調(diào)用start();初始化NetworkDispatcher,然后調(diào)用start();
上面的轉(zhuǎn)發(fā)器呢,都是線程,可以看到,這里開(kāi)了幾個(gè)線程在幫助我們工作,具體的源碼,我們一會(huì)在看。
好了,到這里,就完成了Volley的初始化的相關(guān)代碼,那么接下來(lái)看初始化ImageLoader相關(guān)源碼。
(二) 初始化ImageLoader
#VolleyHelper mImageLoader = new ImageLoader(mReqQueue, new ImageCache() { private final LruCache<String, Bitmap> mLruCache = new LruCache<String, Bitmap>( (int) (Runtime.getRuntime().maxMemory() / 10)) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; @Override public void putBitmap(String url, Bitmap bitmap) { mLruCache.put(url, bitmap); } @Override public Bitmap getBitmap(String url) { return mLruCache.get(url); } }); #ImageLoader public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; }
很簡(jiǎn)單,就是根據(jù)我們初始化的RequestQueue和LruCache初始化了一個(gè)ImageLoader。
(三) 加載圖片
我們?cè)诩虞d圖片時(shí),調(diào)用的是:
# VolleyHelper getInstance().getImageLoader().get(url, new ImageLoader.ImageListener());
接下來(lái)看get方法:
#ImageLoader public ImageContainer get(String requestUrl, final ImageListener listener) { return get(requestUrl, listener, 0, 0); } public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); } public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight, ScaleType scaleType) { // only fulfill requests that were initiated from the main thread. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // Try to look up the request in the cache of remote images. Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { // Return the cached bitmap. ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); imageListener.onResponse(container, true); return container; } // The bitmap did not exist in the cache, fetch it! ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // Update the caller to let them know that they should use the default bitmap. imageListener.onResponse(imageContainer, true); // Check to see if a request is already in-flight. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // If it is, add this request to the list of listeners. request.addContainer(imageContainer); return imageContainer; } // The request is not already in flight. Send the new request to the network and // track it. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; }
可以看到get方法,首先通過(guò)throwIfNotOnMainThread()方法限制必須在UI線程調(diào)用;
然后根據(jù)傳入的參數(shù)計(jì)算cacheKey,獲取cache;
=>如果cache存在,直接將返回結(jié)果封裝為一個(gè)ImageContainer(cachedBitmap, requestUrl),然后直接回調(diào)imageListener.onResponse(container, true);我們就可以設(shè)置圖片了。
=>如果cache不存在,初始化一個(gè)ImageContainer(沒(méi)有bitmap),然后直接回調(diào),imageListener.onResponse(imageContainer, true);,這里為了讓大家在回調(diào)中判斷,然后設(shè)置默認(rèn)圖片(所以,大家在自己實(shí)現(xiàn)listener的時(shí)候,別忘了判斷resp.getBitmap()!=null);
接下來(lái)檢查該url是否早已加入了請(qǐng)求對(duì)了,如果早已加入呢,則將剛初始化的ImageContainer加入BatchedImageRequest,返回結(jié)束。
如果是一個(gè)新的請(qǐng)求,則通過(guò)makeImageRequest創(chuàng)建一個(gè)新的請(qǐng)求,然后將這個(gè)請(qǐng)求分別加入mRequestQueue和mInFlightRequests,注意mInFlightRequests中會(huì)初始化一個(gè)BatchedImageRequest,存儲(chǔ)相同的請(qǐng)求隊(duì)列。
這里注意mRequestQueue是個(gè)對(duì)象,并不是隊(duì)列數(shù)據(jù)結(jié)構(gòu),所以我們要看下add方法
#RequestQueue public <T> Request<T> add(Request<T> request) { // Tag the request as belonging to this queue and add it to the set of current requests. request.setRequestQueue(this); synchronized (mCurrentRequests) { mCurrentRequests.add(request); } // Process requests in the order they are added. request.setSequence(getSequenceNumber()); request.addMarker("add-to-queue"); // If the request is uncacheable, skip the cache queue and go straight to the network. if (!request.shouldCache()) { mNetworkQueue.add(request); return request; } // Insert request into stage if there's already a request with the same cache key in flight. synchronized (mWaitingRequests) { String cacheKey = request.getCacheKey(); if (mWaitingRequests.containsKey(cacheKey)) { // There is already a request in flight. Queue up. Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey); if (stagedRequests == null) { stagedRequests = new LinkedList<Request<?>>(); } stagedRequests.add(request); mWaitingRequests.put(cacheKey, stagedRequests); if (VolleyLog.DEBUG) { VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); } } else { // Insert 'null' queue for this cacheKey, indicating there is now a request in // flight. mWaitingRequests.put(cacheKey, null); mCacheQueue.add(request); } return request; } }
這里首先將請(qǐng)求加入mCurrentRequests,這個(gè)mCurrentRequests保存了所有需要處理的Request,主要為了提供cancel的入口。
如果該請(qǐng)求不應(yīng)該被緩存則直接加入mNetworkQueue,然后返回。
然后判斷該請(qǐng)求是否有相同的請(qǐng)求正在被處理,如果有則加入mWaitingRequests;如果沒(méi)有,則
加入mWaitingRequests.put(cacheKey, null)和mCacheQueue.add(request)。
ok,到這里我們就分析完成了直觀的代碼,但是你可能會(huì)覺(jué)得,那么到底是在哪里觸發(fā)的網(wǎng)絡(luò)請(qǐng)求,加載圖片呢?
那么,首先你應(yīng)該知道,我們需要加載圖片的時(shí)候,會(huì)makeImageRequest然后將這個(gè)請(qǐng)求加入到各種隊(duì)列,主要包含mCurrentRequests、mCacheQueue。
然后,還記得我們初始化RequestQueue的時(shí)候,啟動(dòng)了幾個(gè)轉(zhuǎn)發(fā)線程嗎?CacheDispatcher和NetworkDispatcher。
其實(shí),網(wǎng)絡(luò)請(qǐng)求就是在這幾個(gè)線程中真正去加載的,我們分別看一下;
(四)CacheDispatcher
看一眼構(gòu)造方法;
#CacheDispatcher public CacheDispatcher( BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue, Cache cache, ResponseDelivery delivery) { mCacheQueue = cacheQueue; mNetworkQueue = networkQueue; mCache = cache; mDelivery = delivery; }
這是一個(gè)線程,那么主要的代碼肯定在run里面。
#CacheDispatcher @Override public void run() { if (DEBUG) VolleyLog.v("start new dispatcher"); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // Make a blocking call to initialize the cache. mCache.initialize(); while (true) { try { // Get a request from the cache triage queue, blocking until // at least one is available. final Request<?> request = mCacheQueue.take(); request.addMarker("cache-queue-take"); // If the request has been canceled, don't bother dispatching it. if (request.isCanceled()) { request.finish("cache-discard-canceled"); continue; } // Attempt to retrieve this item from cache. Cache.Entry entry = mCache.get(request.getCacheKey()); if (entry == null) { request.addMarker("cache-miss"); // Cache miss; send off to the network dispatcher. mNetworkQueue.put(request); continue; } // If it is completely expired, just send it to the network. if (entry.isExpired()) { request.addMarker("cache-hit-expired"); request.setCacheEntry(entry); mNetworkQueue.put(request); continue; } // We have a cache hit; parse its data for delivery back to the request. request.addMarker("cache-hit"); Response<?> response = request.parseNetworkResponse( new NetworkResponse(entry.data, entry.responseHeaders)); request.addMarker("cache-hit-parsed"); if (!entry.refreshNeeded()) { // Completely unexpired cache hit. Just deliver the response. mDelivery.postResponse(request, response); } else { // Soft-expired cache hit. We can deliver the cached response, // but we need to also send the request to the network for // refreshing. request.addMarker("cache-hit-refresh-needed"); request.setCacheEntry(entry); // Mark the response as intermediate. response.intermediate = true; // Post the intermediate response back to the user and have // the delivery then forward the request along to the network. mDelivery.postResponse(request, response, new Runnable() { @Override public void run() { try { mNetworkQueue.put(request); } catch (InterruptedException e) { // Not much we can do about this. } } }); } } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { return; } continue; } } }
ok,首先要明確這個(gè)緩存指的是硬盤(pán)緩存(目錄為context.getCacheDir()/volley),內(nèi)存緩存在ImageLoader那里已經(jīng)判斷過(guò)了。
可以看到這里是個(gè)無(wú)限循環(huán),不斷的從mCacheQueue去取出請(qǐng)求,如果請(qǐng)求已經(jīng)被取消就直接結(jié)束;
接下來(lái)從緩存中獲?。?/p>
=>如果沒(méi)有取到,則加入mNetworkQueue
=>如果緩存過(guò)期,則加入mNetworkQueue
否則,就是取到了可用的緩存了;調(diào)用request.parseNetworkResponse解析從緩存中取出的data和responseHeaders;接下來(lái)判斷TTL(主要還是判斷是否過(guò)期),如果沒(méi)有過(guò)期則直接通過(guò)mDelivery.postResponse轉(zhuǎn)發(fā),然后回調(diào)到UI線程;如果ttl不合法,回調(diào)完成后,還會(huì)將該請(qǐng)求加入mNetworkQueue。
好了,這里其實(shí)就是如果拿到合法的緩存,則直接轉(zhuǎn)發(fā)到UI線程;反之,則加入到NetworkQueue.
接下來(lái)我們看NetworkDispatcher。
(五)NetworkDispatcher
與CacheDispatcher類似,依然是個(gè)線程,核心代碼依然在run中;
# NetworkDispatcher //new NetworkDispatcher(mNetworkQueue, mNetwork,mCache, mDelivery) public NetworkDispatcher(BlockingQueue<Request<?>> queue, Network network, Cache cache, ResponseDelivery delivery) { mQueue = queue; mNetwork = network; mCache = cache; mDelivery = delivery; } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); while (true) { long startTimeMs = SystemClock.elapsedRealtime(); Request<?> request; try { // Take a request from the queue. request = mQueue.take(); } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { return; } continue; } try { request.addMarker("network-queue-take"); // If the request was cancelled already, do not perform the // network request. if (request.isCanceled()) { request.finish("network-discard-cancelled"); continue; } addTrafficStatsTag(request); // Perform the network request. NetworkResponse networkResponse = mNetwork.performRequest(request); request.addMarker("network-http-complete"); // If the server returned 304 AND we delivered a response already, // we're done -- don't deliver a second identical response. if (networkResponse.notModified && request.hasHadResponseDelivered()) { request.finish("not-modified"); continue; } // Parse the response here on the worker thread. Response<?> response = request.parseNetworkResponse(networkResponse); request.addMarker("network-parse-complete"); // Write to cache if applicable. // TODO: Only update cache metadata instead of entire record for 304s. if (request.shouldCache() && response.cacheEntry != null) { mCache.put(request.getCacheKey(), response.cacheEntry); request.addMarker("network-cache-written"); } // Post the response back. request.markDelivered(); mDelivery.postResponse(request, response); } catch (VolleyError volleyError) { volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); parseAndDeliverNetworkError(request, volleyError); } catch (Exception e) { VolleyLog.e(e, "Unhandled exception %s", e.toString()); VolleyError volleyError = new VolleyError(e); volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); mDelivery.postError(request, volleyError); } } }
看代碼前,我們首先想一下邏輯,正常情況下我們會(huì)取出請(qǐng)求,讓network去請(qǐng)求處理我們的請(qǐng)求,處理完成以后呢:加入緩存,然后轉(zhuǎn)發(fā)。
那么看下是不是:
首先取出請(qǐng)求;然后通過(guò)mNetwork.performRequest(request)處理我們的請(qǐng)求,拿到NetworkResponse;接下來(lái),使用request去解析我們的NetworkResponse。
拿到Response以后,判斷是否應(yīng)該緩存,如果需要,則緩存。
最后mDelivery.postResponse(request, response);轉(zhuǎn)發(fā);
ok,和我們的預(yù)期差不多。
這樣的話,我們的Volley去加載圖片的核心邏輯就分析完成了,簡(jiǎn)單總結(jié)下:
首先初始化RequestQueue,主要就是開(kāi)啟幾個(gè)Dispatcher線程,線程會(huì)不斷讀取請(qǐng)求(使用的阻塞隊(duì)列,沒(méi)有消息則阻塞)
當(dāng)我們發(fā)出請(qǐng)求以后,會(huì)根據(jù)url,ImageView屬性等,構(gòu)造出一個(gè)cacheKey,然后首先從LruCache中獲?。ㄟ@個(gè)緩存我們自己構(gòu)建的,凡是實(shí)現(xiàn)ImageCache接口的都合法);如果沒(méi)有取到,則判斷是否存在硬盤(pán)緩存,這一步是從getCacheDir里面獲?。J(rèn)5M);如果沒(méi)有取到,則從網(wǎng)絡(luò)請(qǐng)求;
不過(guò),可以發(fā)現(xiàn)的是Volley的圖片加載,并沒(méi)有LIFO這種策略;貌似對(duì)于圖片的下載,也是完整的加到內(nèi)存,然后壓縮,這么看,對(duì)于巨圖、大文件這樣的就廢了;
看起來(lái)還是蠻簡(jiǎn)單的,不過(guò)看完以后,對(duì)于如何更好的時(shí)候該庫(kù)以及如何去設(shè)計(jì)圖片加載庫(kù)還是有很大的幫助的;
如果有興趣,大家還可以在看源碼分析的同時(shí),想想某些細(xì)節(jié)的實(shí)現(xiàn),比如:
Dispatcher都是一些無(wú)限循環(huán)的線程,可以去看看Volley如何保證其關(guān)閉的。
對(duì)于圖片壓縮的代碼,可以在ImageRequest的parseNetworkResponse里面去看看,是如何壓縮的。
so on…
最后貼個(gè)大概的流程圖,方便記憶:
- Android Glide圖片加載(加載監(jiān)聽(tīng)、加載動(dòng)畫(huà))
- Android開(kāi)發(fā)中ImageLoder進(jìn)行圖片加載和緩存
- Android圖片加載緩存框架Glide
- Android圖片加載利器之Picasso基本用法
- Android 常見(jiàn)的圖片加載框架詳細(xì)介紹
- Android圖片加載框架Glide的基本用法介紹
- Android中RecyclerView 滑動(dòng)時(shí)圖片加載的優(yōu)化
- Android程序開(kāi)發(fā)ListView+Json+異步網(wǎng)絡(luò)圖片加載+滾動(dòng)翻頁(yè)的例子(圖片能緩存,圖片不錯(cuò)亂)
- android異步加載圖片并緩存到本地實(shí)現(xiàn)方法
- Android加載大分辨率圖片到手機(jī)內(nèi)存中的實(shí)例方法
- Android ListView實(shí)現(xiàn)ImageLoader圖片加載的方法
相關(guān)文章
android手機(jī)獲取唯一標(biāo)識(shí)的方法
這篇文章主要 為大家詳細(xì)介紹了android手機(jī)獲取唯一標(biāo)識(shí)的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Android開(kāi)發(fā)常見(jiàn)錯(cuò)誤小結(jié)
這篇文章主要介紹了Android開(kāi)發(fā)常見(jiàn)錯(cuò)誤,實(shí)例分析了常見(jiàn)的Android開(kāi)發(fā)中遇到的錯(cuò)誤,對(duì)Android開(kāi)發(fā)有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-05-05Kotlin基礎(chǔ)通關(guān)之字符串與數(shù)字類型
這篇文章主要介紹了Kotlin基礎(chǔ)知識(shí)中的字符串與數(shù)字類型,編程中的入門(mén)知識(shí),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Android 3D滑動(dòng)菜單完全解析 Android實(shí)現(xiàn)推拉門(mén)式的立體特效
這篇文章主要為大家詳細(xì)介紹了Android 3D滑動(dòng)菜單,Android實(shí)現(xiàn)推拉門(mén)式的立體特效,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11Android判斷后臺(tái)服務(wù)是否開(kāi)啟的兩種方法實(shí)例詳解
這篇文章主要介紹了Android判斷后臺(tái)服務(wù)是否開(kāi)啟的方法的相關(guān)資料,這里提供了兩種方法及實(shí)例,需要的朋友可以參考下2017-07-07分享一個(gè)輕量級(jí)圖片加載類 ImageLoader
這篇文章給大家分享一個(gè)輕量級(jí)圖片加載類 ImageLoader,需要的朋友可以參考下2016-08-08