UIL是一個(gè)老牌的開源圖片加載框架,前幾年做Android開發(fā)的小伙伴肯定都非常熟悉忍级。雖然現(xiàn)在UIL的作者已經(jīng)停止進(jìn)行代碼維護(hù)失球,而且后面又有很多優(yōu)秀的開源圖片加載框架推出,比如Glide霸奕、Picasso溜宽、Fresco等。但是這并不妨礙我們對于UIL的基本使用质帅。今天就帶著大家一起來從源碼的角度來認(rèn)識一下Universal_Image_Loader适揉。
一、初識UIL
UIL的基本特性煤惩,這里簡單羅列一下:
- 多線程下載圖片嫉嘀,圖片可以來源于網(wǎng)絡(luò),文件系統(tǒng)魄揉,項(xiàng)目文件夾assets中以及drawable中等
- 支持隨意的配置ImageLoader吃沪,例如線程池,圖片下載器什猖,內(nèi)存緩存策略票彪,硬盤緩存策略红淡,圖片顯示選項(xiàng)以及其他的一些配置
- 支持圖片的內(nèi)存緩存,文件系統(tǒng)緩存或者SD卡緩存
- 支持圖片下載過程的監(jiān)聽
- 根據(jù)控件(ImageView)的大小對Bitmap進(jìn)行裁剪降铸,減少Bitmap占用過多的內(nèi)存
- 較好的控制圖片的加載過程在旱,例如暫停圖片加載,重新開始加載圖片推掸,一般使用在ListView,GridView中桶蝎,滑動過程中暫停加載圖片,停止滑動的時(shí)候去加載圖片
- 提供在較慢的網(wǎng)絡(luò)下對圖片進(jìn)行加載
基本使用方法:
- 在Gradle的dependency下加入
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3'
- 加入權(quán)限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" />
- 在全局Application中new一個(gè)ImageLoder對象谅畅,這個(gè)對象是單例模式的:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//創(chuàng)建默認(rèn)的ImageLoader配置參數(shù)
ImageLoaderConfiguration configuration = ImageLoaderConfiguration
.createDefault(this);
//進(jìn)行初始化操作
ImageLoader.getInstance().init(configuration);
}
}
注意:
- 只能配置一次登渣,如多次配置,則默認(rèn)第一次的配置參數(shù)毡泻。
- 這里主要是imageloader的配置參數(shù)是個(gè)重點(diǎn)胜茧。
一般用默認(rèn)配置就ok。但也提供了一些特定的設(shè)置仇味,只有真的需要的時(shí)候才用呻顽。 - 下面是全部配置,如果需要單獨(dú)設(shè)定某個(gè)值得時(shí)候可以使用丹墨。
具體意思廊遍,根據(jù)方法名自己理解,或者百度贩挣。這里不再一一講解.
File cacheDir = StorageUtils.getCacheDirectory(context); //緩存文件夾路徑
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.memoryCacheExtraOptions(480, 800) // default = device screen dimensions 內(nèi)存緩存文件的最大長寬
.diskCacheExtraOptions(480, 800, null) // 本地緩存的詳細(xì)信息(緩存的最大長寬)喉前,最好不要設(shè)置這個(gè)
.taskExecutor(...)
.taskExecutorForCachedImages(...)
.threadPoolSize(3) // default 線程池內(nèi)加載的數(shù)量
.threadPriority(Thread.NORM_PRIORITY - 2) // default 設(shè)置當(dāng)前線程的優(yōu)先級
.tasksProcessingOrder(QueueProcessingType.FIFO) // default
.denyCacheImageMultipleSizesInMemory()
.memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //可以通過自己的內(nèi)存緩存實(shí)現(xiàn)
.memoryCacheSize(2 * 1024 * 1024) // 內(nèi)存緩存的最大值
.memoryCacheSizePercentage(13) // default
.diskCache(new UnlimitedDiscCache(cacheDir)) // default 可以自定義緩存路徑
.diskCacheSize(50 * 1024 * 1024) // 50 Mb sd卡(本地)緩存的最大值
.diskCacheFileCount(100) // 可以緩存的文件數(shù)量
// default為使用HASHCODE對UIL進(jìn)行加密命名, 還可以用MD5(new Md5FileNameGenerator())加密
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator())
.imageDownloader(new BaseImageDownloader(context)) // default
.imageDecoder(new BaseImageDecoder()) // default
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
.writeDebugLogs() // 打印debug log
.build(); //開始構(gòu)建
- 配置加載圖片的屬性王财,這里主要是一些加載失敗的默認(rèn)圖片被饿,加載過程中的圖片,緩存位置搪搏,bitmap屬性等狭握。這里,我在使用的時(shí)候定義了三個(gè)DisplayImageOptions對象疯溺,分別是加載大圖论颅、中圖、小圖囱嫩∈逊瑁基本涵蓋了app中圖片的使用。下面給出一個(gè)小圖的options
DisplayImageOptions smallImageOptions = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.default_pic_small)
.showImageOnFail(R.drawable.default_pic_small)
.showImageForEmptyUri(R.drawable.default_pic_small)
.cacheInMemory(true)
.cacheOnDisk(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
- 開始加載圖片
- 網(wǎng)絡(luò)圖片
ImageLoader.getInstance().displayImage(url, imageView, option);
- drawable圖片(非9.png圖片)
String drawableUrl = Scheme.DRAWABLE.wrap("R.drawable.image");
ImageLoader.getInstance().displayImage(drawableUrl , imageView, option);
或者
ImageLoader.getInstance().displayImage("drawable://" + imageId,
imageView);
- 本地圖片
String imageUrl = ImageDownloader.Scheme.FILE.wrap("/mnt/sdcard/image.png");
ImageLoader.getInstance().displayImage(imageUrl, imageView, option);
- assets
String assetsUrl = Scheme.ASSETS.wrap("image.png");
ImageLoader.getInstance().displayImage(assetsUrl , imageView, option);
- Content provider
String contentprividerUrl = "content://media/external/audio/albumart/13";
ImageLoader.getInstance().displayImage(contentprividerUrl , imageView, option);
- GirdView,ListView加載圖片
當(dāng)我們快速滑動GridView墨闲,ListView今妄,我們希望能停止圖片的加載,而在GridView,ListView停止滑動的時(shí)候加載當(dāng)前界面的圖片盾鳞。我們只需要在初始化ListView和GridView的時(shí)候犬性,加上下面兩行配置:
listView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));
gridView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));
二、UID的圖片緩存策略
- 圖片緩存的概念腾仅,簡單點(diǎn)來說當(dāng)你第一次打開某個(gè)界面的時(shí)候乒裆,圖片可能要等待幾秒鐘才能加載完成,但當(dāng)你第二次進(jìn)入時(shí)推励,圖片直接就可以顯示出來鹤耍,相對于第一次加載快了很多。這里運(yùn)用的就是圖片緩存技術(shù)验辞。
- 三級緩存稿黄。一般的策略是先從內(nèi)存中找,沒有的話再去本地disk中找跌造,再沒有的話就從網(wǎng)絡(luò)獲取杆怕。下面的文章主要說一下內(nèi)存緩存和磁盤緩存,網(wǎng)絡(luò)緩存就是重新加載了鼻听。三級緩存的特點(diǎn):
- 內(nèi)存緩存 優(yōu)先加載财著,速度最快
- 本地緩存 次優(yōu)先加載 速度稍快
- 網(wǎng)絡(luò)緩存 最后加載 速度由網(wǎng)絡(luò)速度決定(浪費(fèi)流量)
三联四、內(nèi)存緩存
- 內(nèi)存緩存中的強(qiáng)引用和弱引用
- 強(qiáng)引用是指創(chuàng)建一個(gè)對象并把這個(gè)對象賦給一個(gè)引用變量撑碴, 強(qiáng)引用有引用變量指向時(shí)永遠(yuǎn)不會被垃圾回收。即使內(nèi)存不足的時(shí)候?qū)幵笀?bào)OOM也不被垃圾回收器回收朝墩,我們new的對象都是強(qiáng)引用
- 弱引用通過weakReference類來實(shí)現(xiàn)醉拓,它具有很強(qiáng)的不確定性,如果垃圾回收器掃描到有著WeakReference的對象收苏,就會將其回收釋放內(nèi)存
-
UIL中的內(nèi)存緩存策略
UIL提供的內(nèi)存緩存策略有下面幾種亿卤,大家可以自己去看下源碼,非常簡單鹿霸。
(1)使用的是強(qiáng)引用緩存- LruMemoryCache(這個(gè)類就是這個(gè)開源框架默認(rèn)的內(nèi)存緩存類排吴,緩存的是bitmap的強(qiáng)引用,下面我會從源碼上面分析這個(gè)類)
(2) 使用強(qiáng)引用和弱引用相結(jié)合的緩存有
- UsingFreqLimitedMemoryCache(如果緩存的圖片總量超過限定值懦鼠,先刪除使用頻率最小的bitmap)
- LRULimitedMemoryCache(這個(gè)也是使用的lru算法钻哩,和LruMemoryCache不同的是,他緩存的是bitmap的弱引用)
- FIFOLimitedMemoryCache(先進(jìn)先出的緩存策略肛冶,當(dāng)超過設(shè)定值街氢,先刪除最先加入緩存的bitmap)
- LargestLimitedMemoryCache(當(dāng)超過緩存限定值,先刪除最大的bitmap對象)
- LimitedAgeMemoryCache(當(dāng) bitmap加入緩存中的時(shí)間超過我們設(shè)定的值睦袖,將其刪除)
(3) 只使用弱引用緩存
- WeakMemoryCache(這個(gè)類緩存bitmap的總大小沒有限制珊肃,唯一不足的地方就是不穩(wěn)定,緩存的圖片容易被回收掉)
-
UIL手動修改內(nèi)存緩存策略
在Application中初始化ImageLoaderConfiguration的時(shí)候,加上memoryCache就可以了伦乔。
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)
.memoryCache(new WeakMemoryCache())
.build();
-
LruMemoryCache 類解析
這里已LruMemoryCache 類為例進(jìn)行分析厉亏,源碼的話,就不貼了评矩。只列出關(guān)鍵的代碼叶堆。
private final LinkedHashMap<String, Bitmap> map;
private final int maxSize;
/** Size of this cache in bytes */
private int size;
LruMemoryCache 緩存了一個(gè)LinkedHashMap<String, Bitmap> map對象,用來保存圖片斥杜。同時(shí)還定義了一個(gè)最大緩存值虱颗,以及一個(gè)當(dāng)前的緩存值。
/**
* Remove the eldest entries until the total of remaining entries is at or below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements.
*/
private void trimToSize(int maxSize) {
while (true) {
String key;
Bitmap value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= sizeOf(key, value);
}
}
}
如果當(dāng)前緩存的bitmap總數(shù)小于設(shè)定值maxSize蔗喂,不做任何處理忘渔,如果當(dāng)前緩存的bitmap總數(shù)大于maxSize,刪除LinkedHashMap中的第一個(gè)元素,size中減去該bitmap對應(yīng)的byte數(shù)
四缰儿、硬盤緩存
- UIL提供的硬盤緩存策略
- FileCountLimitedDiscCache(可以設(shè)定緩存圖片的個(gè)數(shù)畦粮,當(dāng)超過設(shè)定值,刪除掉最先加入到硬盤的文件)
- LimitedAgeDiscCache(設(shè)定文件存活的最長時(shí)間乖阵,當(dāng)超過這個(gè)值宣赔,就刪除該文件)
- TotalSizeLimitedDiscCache(設(shè)定緩存bitmap的最大值,當(dāng)超過這個(gè)值瞪浸,刪除最先加入到硬盤的文件)
- UnlimitedDiscCache(這個(gè)緩存類沒有任何的限制)
五儒将、UIL加載一張網(wǎng)絡(luò)圖片的過程
- 先看下我們平常使用加載圖片的代碼:
ImageLoader.getInstance().displayImage(imageUrl, imageView, options);
- 再看下displayImage的源碼:
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {
displayImage(uri, new ImageViewAware(imageView), options, null, null);
}
這個(gè)ImageViewAware是什么?看下源碼的注釋:
"Wrapper for Android {@link android.widget.ImageView ImageView}. Keeps weak reference of ImageView to prevent memory leaks."
將ImageView的強(qiáng)引用變成弱引用对蒲,當(dāng)內(nèi)存不足的時(shí)候钩蚊,可以更好的回收ImageView對象,還有就是獲取ImageView的寬度和高度蹈矮。使得我們可以根據(jù)ImageView的寬高去對圖片進(jìn)行一個(gè)裁剪砰逻,減少內(nèi)存的使用。
看下ImageLoader中的displayImage方法泛鸟,上代碼:
/**
* Adds display image task to execution pool. Image will be set to ImageAware when it's turn.<br />
* <b>NOTE:</b> {@link #init(ImageLoaderConfiguration)} method must be called before this method call
*
* @param uri Image URI (i.e. "http://site.com/image.png", "file:///mnt/sdcard/image.png")
* @param imageAware {@linkplain com.nostra13.universalimageloader.core.imageaware.ImageAware Image aware view}
* which should display image
* @param options {@linkplain com.nostra13.universalimageloader.core.DisplayImageOptions Options} for image
* decoding and displaying. If <b>null</b> - default display image options
* {@linkplain ImageLoaderConfiguration.Builder#defaultDisplayImageOptions(DisplayImageOptions)
* from configuration} will be used.
* @param listener {@linkplain ImageLoadingListener Listener} for image loading process. Listener fires
* events on UI thread if this method is called on UI thread.
* @param progressListener {@linkplain com.nostra13.universalimageloader.core.listener.ImageLoadingProgressListener
* Listener} for image loading progress. Listener fires events on UI thread if this method
* is called on UI thread. Caching on disk should be enabled in
* {@linkplain com.nostra13.universalimageloader.core.DisplayImageOptions options} to make
* this listener work.
* @throws IllegalStateException if {@link #init(ImageLoaderConfiguration)} method wasn't called before
* @throws IllegalArgumentException if passed <b>imageAware</b> is null
*/
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = emptyListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
if (TextUtils.isEmpty(uri)) {
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}
ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
我大致說下處理流程:
- 檢查配置是否初始化蝠咆;
- 檢查imageAware,也就是ImageView是否為空北滥;
- 檢查ImageLoadingListener和ImageLoadingProgressListener是否為空刚操,如果為空就使用默認(rèn)的配置。
- 判斷圖片地址碑韵。如果為空就顯示配置的空圖片赡茸,并取消網(wǎng)絡(luò)任務(wù),同時(shí)調(diào)用ImageLoadingListener的onLoadingComplete方法祝闻,結(jié)束本次加載占卧。如果圖片地址不為空遗菠,那么進(jìn)行下面的步驟
- 將ImageView的寬高封裝成ImageSize對象.如果獲取ImageView的寬高為0,就會使用手機(jī)屏幕的寬高作為ImageView的寬高华蜒。
- 從內(nèi)存中查找這個(gè)圖片辙纬。如果從內(nèi)存中找到了,那么再判斷顯示圖片的options.shouldPostProcess()是否為true叭喜。postProcessor這個(gè)屬性主要是判斷是否需要對取到的這個(gè)圖片進(jìn)行處理贺拣,比如切成圓角邊等。這個(gè)對應(yīng)的需要自己實(shí)現(xiàn)BitmapProcessor接口來處理這個(gè)bitmap捂蕴。如果需要處理圖片譬涡,就進(jìn)行處理,然后顯示啥辨。如果不需要處理涡匀,就直接顯示到imageAware上去。加載完成溉知。
- 如果沒有從內(nèi)存中找到這張圖片陨瘩,那么就實(shí)例化一個(gè)LoadAndDisplayImageTask對象。這個(gè)對象實(shí)現(xiàn)了Runnable,如果配置了isSyncLoading為true, 直接執(zhí)行LoadAndDisplayImageTask的run方法级乍,表示同步舌劳,默認(rèn)是false,將LoadAndDisplayImageTask提交給線程池對象。這個(gè)對象的主要作用是去disk或者網(wǎng)絡(luò)中去加載圖片玫荣。
下面看下LoadAndDisplayImageTask是如何從disk或者網(wǎng)絡(luò)加載圖片的甚淡。先看它的run方法
@Override
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return;
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}
loadFromUriLock.lock();
Bitmap bmp;
try {
checkTaskNotActual();
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
checkTaskInterrupted();
if (options.shouldPreProcess()) {
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
}
if (bmp != null && options.isCacheInMemory()) {
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {
loadedFrom = LoadedFrom.MEMORY_CACHE;
L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
}
if (bmp != null && options.shouldPostProcess()) {
L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPostProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
}
}
checkTaskNotActual();
checkTaskInterrupted();
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
}
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
- if (waitIfPaused()) return;這句話主要是用在ListView或者GridView在滑動過程中,不去加載圖片崇决。這里需要配合
ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling ))
- if (waitIfPaused()) return;和if (delayIfNeed()) return;的返回值最終都由一個(gè)方法isTaskNotActual決定材诽〉状欤看下這個(gè)方法的源碼
private boolean isTaskNotActual() {
return isViewCollected() || isViewReused();
}
isViewCollected()是判斷我們ImageView是否被垃圾回收器回收了恒傻,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了建邓,isViewReused()判斷該ImageView是否被重用盈厘,被重用run()方法也直接返回。被重用的情況主要發(fā)生在ListView和GridView當(dāng)中官边,如果當(dāng)前加載的ImageView已經(jīng)被重用了沸手,那么就沒必要繼續(xù)加載之前的圖片了,所以直接返回注簿。
- 檢查完可以去加載圖片了契吉。先加一個(gè)鎖ReentrantLock。這個(gè)鎖的作用就是相同url的請求只執(zhí)行一次诡渴。比如listview中的一個(gè)item需要從網(wǎng)上加載圖片捐晶。我們用手機(jī)滑動ListView讓這個(gè)item,一會滑入界面,然后滑出后再馬上滑入惑灵。重復(fù)幾次的話山上,就會有很多個(gè)相同url的請求。加了這個(gè)鎖的作用英支,這些請求只執(zhí)行一遍佩憾,而不是全部去執(zhí)行一遍。
再看下用了這個(gè)url請求后干了什么 - checkTaskNotActual()干花。這個(gè)也是檢查view是否被回收和復(fù)用妄帘。
- 再從緩存中檢查一遍,如果緩存中沒有池凄,那么去tryLoadBitmap();這個(gè)就是加載的方法寄摆。大概的流程就是先從disk緩存中獲取圖片,如果沒有再去從網(wǎng)絡(luò)中獲取修赞,然后將bitmap保存在文件系統(tǒng)中婶恼。
- 從服務(wù)器上獲取圖片保存到本地是調(diào)用了tryCacheImageOnDisk這個(gè)方法。這個(gè)方法里調(diào)用了downloadImage這個(gè)方法去下載圖片柏副。
- 剩下的就是顯示圖片了勾邦。 同樣會檢查一遍view有沒有被回收,被復(fù)用割择。
至此眷篇,顯示圖片的過程就結(jié)束了。
筆者在寫這篇博客的過程中荔泳,從以下兩篇文章中學(xué)到了很多有價(jià)值的東西蕉饼,十分感謝!
Android 開源框架Universal-Image-Loader完全解析
Android開源框架Universal-Image-Loader詳解