一起動(dòng)手編寫Android圖片加載框架
開發(fā)一個(gè)簡(jiǎn)潔而實(shí)用的Android圖片加載緩存框架,并在內(nèi)存占用與加載圖片所需時(shí)間這兩個(gè)方面與主流圖片加載框架之一Universal Image Loader做出比較,來幫助我們量化這個(gè)框架的性能。通過開發(fā)這個(gè)框架,我們可以進(jìn)一步深入了解Android中的Bitmap操作、LruCache、LruDiskCache,讓我們以后與Bitmap打交道能夠更加得心應(yīng)手。若對(duì)Bitmap的大小計(jì)算及inSampleSize計(jì)算還不太熟悉,請(qǐng)參考這里:高效加載Bitmap。由于個(gè)人水平有限,敘述中必然存在不準(zhǔn)確或是不清晰的地方,希望大家能夠指出,謝謝大家。
一、圖片加載框架需求描述
在著手進(jìn)行實(shí)際開發(fā)工作之前,我們先來明確以下我們的需求。通常來說,一個(gè)實(shí)用的圖片加載框架應(yīng)該具備以下2個(gè)功能:
圖片的加載:包括從不同來源(網(wǎng)絡(luò)、文件系統(tǒng)、內(nèi)存等),支持同步及異步方式,支持對(duì)圖片的壓縮等等;
圖片的緩存:包括內(nèi)存緩存和磁盤緩存。
下面我們來具體描述下這些需求。
1. 圖片的加載
(1)同步加載與異步加載
我們先來簡(jiǎn)單的復(fù)習(xí)下同步與異步的概念:
同步:發(fā)出了一個(gè)“調(diào)用”后,需要等到該調(diào)用返回才能繼續(xù)執(zhí)行;
異步:發(fā)出了一個(gè)“調(diào)用”后,無需等待該調(diào)用返回就能繼續(xù)執(zhí)行。
同步加載就是我們發(fā)出加載圖片這個(gè)調(diào)用后,直到完成加載我們才繼續(xù)干別的活,否則就一直等著;異步加載也就是發(fā)出加載圖片這個(gè)調(diào)用后我們可以直接去干別的活。
(2)從不同的來源加載
我們的應(yīng)用有時(shí)候需要從網(wǎng)絡(luò)上加載圖片,有時(shí)候需要從磁盤加載,有時(shí)候又希望從內(nèi)存中直接獲取。因此一個(gè)合格的圖片加載框架應(yīng)該支持從不同的來源來加載一個(gè)圖片。對(duì)于網(wǎng)絡(luò)上的圖片,我們可以使用HttpURLConnection來下載并解析;對(duì)于磁盤中的圖片,我們可以使用BitmapFactory的decodeFile方法;對(duì)于內(nèi)存中的Bitmap,我們直接就可以獲取。
(3)圖片的壓縮
關(guān)于對(duì)圖片的壓縮,主要的工作是計(jì)算出inSampleSize,剩下的細(xì)節(jié)在下面實(shí)現(xiàn)部分我們會(huì)介紹。
2. 圖片的緩存
緩存功能對(duì)于一個(gè)圖片加載框架來說是十分必要的,因?yàn)閺木W(wǎng)絡(luò)上加載圖片既耗時(shí)耗電又費(fèi)流量。通常我們希望把已經(jīng)加載過的圖片緩存在內(nèi)存或磁盤中,這樣當(dāng)我們?cè)俅涡枰虞d相同的圖片時(shí)可以直接從內(nèi)存緩存或磁盤緩存中獲取。
(1)內(nèi)存緩存
訪問內(nèi)存的速度要比訪問磁盤快得多,因此我們傾向于把更加常用的圖片直接緩存在內(nèi)存中,這樣加載速度更快,內(nèi)存緩存的不足在于由于內(nèi)存空間有限,能夠緩存的圖片也比較少。我們可以選擇使用SDK提供的LruCache類來實(shí)現(xiàn)內(nèi)存緩存,這個(gè)類使用了LRU算法來管理緩存對(duì)象,LRU算法即Least Recently Used(最近最少使用),它的主要思想是當(dāng)緩存空間已滿時(shí),移除最近最少使用的緩存對(duì)象。關(guān)于LruCache類的具體使用我們下面會(huì)進(jìn)行詳細(xì)介紹。
(2)磁盤緩存
磁盤緩存的優(yōu)勢(shì)在于能夠緩存的圖片數(shù)量比較多,不足就是磁盤IO的速度比較慢。磁盤緩存我們可以用DiskLruCache來實(shí)現(xiàn),這個(gè)類不屬于Android SDK,文末給出的本文示例代碼的地址,其中包含了DiskLruCache。
DisLruCache同樣使用了LRU算法來管理緩存,關(guān)于它的具體使用我們會(huì)在后文進(jìn)行介紹。
二、緩存類使用介紹
1. LruCache的使用
首先我們來看一下LruCache類的定義:
public class LruCache<K, V> { private final LinkedHashMap<K, V> map; ... public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); } ... }
由以上代碼我們可以知道,LruCache是個(gè)泛型類,它的內(nèi)部使用一個(gè)LinkedHashMap來管理緩存對(duì)象。
(1)初始化LruCache
初始化LruCache的慣用代碼如下所示:
//獲取當(dāng)前進(jìn)程的可用內(nèi)存(單位KB) int maxMemory = (int) (Runtime.getRuntime().maxMemory() /1024); int memoryCacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(memoryCacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; } };
在以上代碼中,我們創(chuàng)建了一個(gè)LruCache實(shí)例,并指定它的maxSize為當(dāng)前進(jìn)程可用內(nèi)存的1/8。我們使用String作為key,value自然是Bitmap。第6行到第8行我們重寫了sizeOf方法,這個(gè)方法被LruCache用來計(jì)算一個(gè)緩存對(duì)象的大小。我們使用了getByteCount方法返回Bitmap對(duì)象以字節(jié)為單位的方法,又除以了1024,轉(zhuǎn)換為KB為單位的大小,以達(dá)到與cacheSize的單位統(tǒng)一。
(2)獲取緩存對(duì)象
LruCache類通過get方法來獲取緩存對(duì)象,get方法的源碼如下:
public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; } /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. */ V createdValue = create(key); if (createdValue == null) { return null; } synchronized (this) { createCount++; mapValue = map.put(key, createdValue); if (mapValue != null) { // There was a conflict so undo that last put map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } }
通過以上代碼我們了解到,首先會(huì)嘗試根據(jù)key獲取相應(yīng)value(第8行),若不存在則會(huì)新建一個(gè)key-value對(duì),并將它放入到LinkedHashMap中。從get方法的實(shí)現(xiàn)我們可以看到,它用synchronized關(guān)鍵字作了同步,因此這個(gè)方法是線程安全的。實(shí)際上,LruCache類對(duì)所有可能涉及并發(fā)數(shù)據(jù)訪問的方法都作了同步。
(3)添加緩存對(duì)象
在添加緩存對(duì)象之前,我們先得確定用什么作為被緩存的Bitmap對(duì)象的key,一種很直接的做法便是使用Bitmap的URL作為key,然而由于URL中存在一些特殊字符,所以可能會(huì)產(chǎn)生一些問題。基于以上原因,我們可以考慮使用URL的md5值作為key,這能夠很好的保證不同的url具有不同的key,而且相同的url得到的key相同。我們自定義一個(gè)getKeyFromUrl方法來通過URI獲取key,該方法的代碼如下:
private String getKeyFromUrl(String url) { String key; try { MessageDigest messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update(url.getBytes()); byte[] m = messageDigest.digest(); return getString(m); } catch (NoSuchAlgorithmException e) { key = String.valueOf(url.hashCode()); } return key; } private static String getString(byte[] b){ StringBuffer sb = new StringBuffer(); for(int i = 0; i < b.length; i ++){ sb.append(b[i]); } return sb.toString(); }
得到了key后,我們可以使用put方法向LruCache內(nèi)部的LinkedHashMap中添加緩存對(duì)象,這個(gè)方法的源碼如下:
public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); return previous; }
從以上代碼我們可以看到這個(gè)方法確實(shí)也作了同步,它將新的key-value對(duì)放入LinkedHashMap后會(huì)返回相應(yīng)key原來對(duì)應(yīng)的value。
(4)刪除緩存對(duì)象
我們可以通過remove方法來刪除緩存對(duì)象,這個(gè)方法的源碼如下:
public final V remove(K key) { if (key == null) { throw new NullPointerException("key == null"); } V previous; synchronized (this) { previous = map.remove(key); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, null); } return previous; }
這個(gè)方法會(huì)從LinkedHashMap中移除指定key對(duì)應(yīng)的value并返回這個(gè)value,我們可以看到它的內(nèi)部還調(diào)用了entryRemoved方法,如果有需要的話,我們可以重寫entryRemoved方法來做一些資源回收的工作。
2. DiskLruCache的使用
(1)初始化DiskLruCache
通過查看DiskLruCache的源碼我們可以發(fā)現(xiàn),DiskLruCache就存在如下一個(gè)私有構(gòu)造方法:
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { this.directory = directory; this.appVersion = appVersion; this.journalFile = new File(directory, JOURNAL_FILE); this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); this.valueCount = valueCount; this.maxSize = maxSize; }
因此我們不能直接調(diào)用構(gòu)造方法來創(chuàng)建DiskLruCache的實(shí)例。實(shí)際上DiskLruCache為我們提供了open靜態(tài)方法來創(chuàng)建一個(gè)DiskLruCache實(shí)例,我們來看一下這個(gè)方法的實(shí)現(xiàn):
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (valueCount <= 0) { throw new IllegalArgumentException("valueCount <= 0"); } // prefer to pick up where we left off DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); if (cache.journalFile.exists()) { try { cache.readJournal(); cache.processJournal(); cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), IO_BUFFER_SIZE); return cache; } catch (IOException journalIsCorrupt) { // System.logW("DiskLruCache " + directory + " is corrupt: " // + journalIsCorrupt.getMessage() + ", removing"); cache.delete(); } } // create a new empty cache directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); return cache; }
從以上代碼中我們可以看到,open方法內(nèi)部調(diào)用了DiskLruCache的構(gòu)造方法,并傳入了我們傳入open方法的4個(gè)參數(shù),這4個(gè)參數(shù)的含義分別如下:
directory:代表緩存文件在文件系統(tǒng)的存儲(chǔ)路徑;
appVersion:代表應(yīng)用版本號(hào),通常設(shè)為1即可;
valueCount:代表LinkedHashMap中每個(gè)節(jié)點(diǎn)上的緩存對(duì)象數(shù)目,通常設(shè)為1即可;
maxSize:代表了緩存的總大小,若緩存對(duì)象的總大小超過了maxSize,DiskLruCache會(huì)自動(dòng)刪去最近最少使用的一些緩存對(duì)象。
以下代碼展示了初始化DiskLruCache的慣用代碼:
File diskCacheDir= getAppCacheDir(mContext, "images"); if (!diskCacheDir.exists()) { diskCacheDir.mkdirs(); } mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
以上代碼中的getAppCacheDir是我們自定義的用來獲取磁盤緩存目錄的方法,它的定義如下:
public static File getAppCacheDir(Context context, String dirName) { String cacheDirString; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { cacheDirString = context.getExternalCacheDir().getPath(); } else { cacheDirString = context.getCacheDir().getPath(); } return new File(cacheDirString + File.separator + dirName); }
接下來我們介紹如何添加、獲取和刪除緩存對(duì)象。
(2)添加緩存對(duì)象
先通過以上介紹的getKeyFromUrl獲取Bitmap對(duì)象對(duì)應(yīng)的key,接下來我們就可以把這個(gè)Bitmap存入磁盤緩存中了。我們通過Editor來向DiskLruCache添加緩存對(duì)象。首先我們要通過edit方法獲取一個(gè)Editor對(duì)象:
String key = getKeyFromUrl(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key);
獲取到Editor對(duì)象后,通過調(diào)用Editor對(duì)象的newOutputStream我們就可以獲取key對(duì)應(yīng)的Bitmap的輸出流,需要注意的是,若我們想通過edit方法獲取的那個(gè)緩存對(duì)象正在被“編輯”,那么edit方法會(huì)返回null。相關(guān)的代碼如下:
if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); //參數(shù)為索引,由于我們創(chuàng)建時(shí)指定一個(gè)節(jié)點(diǎn)只有一個(gè)緩存對(duì)象,所以傳入0即可 }
獲取了輸出流后,我們就可以向這個(gè)輸出流中寫入圖片數(shù)據(jù),成功寫入后調(diào)用commit方法即可,若寫入失敗則調(diào)用abort方法進(jìn)行回退。相關(guān)的代碼如下:
//getStream為我們自定義的方法,它通過URL獲取輸入流并寫入outputStream,具體實(shí)現(xiàn)后文會(huì)給出 if (getStreamFromUrl(url, outputStream)) { editor.commit(); } else { //返回false表示寫入outputStream未成功,因此調(diào)用abort方法回退整個(gè)操作 editor.abort(); } mDiskLruCache.flush(); //將內(nèi)存中的操作記錄同步到日志文件中
下面我們來看一下getStream方法的實(shí)現(xiàn),這個(gè)方法實(shí)現(xiàn)很直接簡(jiǎn)單,就是創(chuàng)建一個(gè)HttpURLConnection,然后獲取InputStream再寫入outputStream,為了提高效率,使用了包裝流。該方法的代碼如下:
public boolean getStreamFromUrl(String urlString, OutputStream outputStream) { HttpURLConnection urlCOnnection = null; BufferedInputStream bis = null; BufferedOutputStream bos = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); bis = new BufferedInputStream(urlConnection.getInputStream(), BUF_SIZE); int byteRead; while ((byteRead = bis.read()) != -1) { bos.write(byteRead); } return true; }catch (IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } //HttpUtils為一個(gè)自定義工具類 HttpUtils.close(bis); HttpUtils.close(bos); } return false; }
經(jīng)過以上的步驟,我們已經(jīng)成功地將圖片寫入了文件系統(tǒng)。
(3)獲取緩存對(duì)象
我們使用DiskLruCache的get方法從中獲取緩存對(duì)象,這個(gè)方法的大致源碼如下:
public synchronized Snapshot get(String key) throws IOException { checkNotClosed(); validateKey(key); Entry entry = lruEntries.get(key); if (entry == null) { return null; } if (!entry.readable) { return null; } /* * Open all streams eagerly to guarantee that we see a single published * snapshot. If we opened streams lazily then the streams could come * from different edits. */ InputStream[] ins = new InputStream[valueCount];19 ... return new Snapshot(key, entry.sequenceNumber, ins); }
我們可以看到,這個(gè)方法最終返回了一個(gè)Snapshot對(duì)象,并以我們要獲取的緩存對(duì)象的key作為構(gòu)造參數(shù)之一。
Snapshot是DiskLruCache的內(nèi)部類,它包含一個(gè)getInputStream方法,通過這個(gè)方法可以獲取相應(yīng)緩存對(duì)象的輸入流,得到了這個(gè)輸入流,我們就可以進(jìn)一步獲取到Bitmap對(duì)象了。在獲取緩存的Bitmap時(shí),我們通常都要對(duì)它進(jìn)行一些預(yù)處理,主要就是通過設(shè)置inSampleSize來適當(dāng)?shù)目s放圖片,以防止出現(xiàn)OOM。我們之前已經(jīng)介紹過如何高效加載Bitmap,在那篇文章里我們的圖片來源于Resources。盡管現(xiàn)在我們的圖片來源是流對(duì)象,但是計(jì)算inSampleSize的方法是一樣的,只不過我們不再使用decodeResource方法而是使用decodeFileDescriptor方法。
相關(guān)的代碼如下:
Bitmap bitmap = null; String key = getKeyFromUrl(url); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(0); //參數(shù)表示索引,同之前的newOutputStream一樣 FileDescriptor fileDescriptor = fileInputStream.getFD(); bitmap = decodeSampledBitmapFromFD(fileDescriptor, dstWidth, dstHeight); if (bitmap != null) { addBitmapToMemoryCache(key, bitmap); } }
第7行我們調(diào)用了decodeSampledBitmapFromFD來從fileInputStream的文件描述符中解析出Bitmap,decodeSampledBitmapFromFD方法的定義如下:
public Bitmap decodeSampledBitmapFromFD(FileDescriptor fd, int dstWidth, int dstHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fd, null, options); //calInSampleSize方法的實(shí)現(xiàn)請(qǐng)見“Android開發(fā)之高效加載Bitmap”這篇博文 options.inSampleSize = calInSampleSize(options, dstWidth, dstHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeFileDescriptor(fd, null, options); }
第9行我們調(diào)用了addBitmapToMemoryCache方法把獲取到的Bitmap加入到內(nèi)存緩存中,關(guān)于這一方法的具體實(shí)現(xiàn)下文會(huì)進(jìn)行介紹。
三、圖片加載框架的具體實(shí)現(xiàn)
1. 圖片的加載
(1)同步加載
同步加載的相關(guān)代碼需要在工作者線程中執(zhí)行,因?yàn)槠渲猩婕暗綄?duì)網(wǎng)絡(luò)的訪問,并且可能是耗時(shí)操作。同步加載的大致步驟如下:首先嘗試從內(nèi)存緩存中加載Bitmap,若不存在再從磁盤緩存中加載,若不存在則從網(wǎng)絡(luò)中獲取并添加到磁盤緩存中。同步加載的代碼如下:
public Bitmap loadBitmap(String url, int dstWidth, int dstHeight) { Bitmap bitmap = loadFromMemory(url); if (bitmap != null) { return bitmap; } //內(nèi)存緩存中不存在相應(yīng)圖片 try { bitmap = loadFromDisk(url, dstWidth, dstHeight); if (bitmap != null) { return bitmap; } //磁盤緩存中也不存在相應(yīng)圖片 bitmap = loadFromNet(url, dstWidth, dstHeight); } catch (IOException e) { e.printStackTrace(); } return bitmap; }
loadBitmapFromNet方法的功能是從網(wǎng)絡(luò)上獲取指定url的圖片,并根據(jù)給定的dstWidth和dstHeight對(duì)它進(jìn)行縮放,返回縮放后的圖片。loadBitmapFromDisk方法則是從磁盤緩存中獲取并縮放,而后返回縮放后的圖片。關(guān)于這兩個(gè)方法的實(shí)現(xiàn)在下面“圖片的緩存”部分我們會(huì)具體介紹。下面我們先來看看異步加載圖片的實(shí)現(xiàn)。
(2)異步加載
異步加載圖片在實(shí)際開發(fā)中更經(jīng)常被使用,通常我們希望圖片加載框架幫我們?nèi)ゼ虞d圖片,我們接著干別的活,等到圖片加載好了,圖片加載框架會(huì)負(fù)責(zé)將它顯示在我們給定的ImageView中。我們可以使用線程池去執(zhí)行異步加載任務(wù),加載好后通過Handler來更新UI(將圖片顯示在ImageView中)。相關(guān)代碼如下所示:
public void displayImage(String url, ImageView imageView, int dstWidth, int widthHeight) { imageView.setTag(IMG_URL, url); Bitmap bitmap = loadFromMemory(url); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } Runnable loadBitmapTask = new Runnable() { @Override public void run() { Bitmap bitmap = loadBitmap(url, dstWidth, dstHeigth); if (bitmap != null) { //Result是我們自定義的類,封裝了返回的Bitmap以及它的URL和作為它的容器的ImageView Result result = new Result(bitmap, url, imageView); //mMainHandler為主線程中創(chuàng)建的Handler Message msg = mMainHandler.obtainMessage(MESSAGE_SEND_RESULT, result); msg.sendToTarget(); } } }; threadPoolExecutor.execute(loadBitmapTask); }
從以上代碼我們可以看到,異步加載與同步加載之間的區(qū)別在于,異步加載把耗時(shí)任務(wù)放入了線程池中執(zhí)行。同步加載需要我們創(chuàng)建一個(gè)線程并在新線程中執(zhí)行l(wèi)oadBitmap方法,使用異步加載我們只需傳入url、imageView等參數(shù),圖片加載框架負(fù)責(zé)使用線程池在后臺(tái)執(zhí)行圖片加載任務(wù),加載成功后會(huì)通過發(fā)送消息給主線程來實(shí)現(xiàn)把Bitmap顯示在ImageView中。我們來簡(jiǎn)單的解釋下obtainMessage這個(gè)方法,我們傳入了兩個(gè)參數(shù),第一個(gè)參數(shù)代表消息的what屬性,這時(shí)個(gè)int值,相當(dāng)于我們給消息定的一個(gè)標(biāo)識(shí),來區(qū)分不同的消息;第二個(gè)參數(shù)代表消息的obj屬性,表示我們附帶的一個(gè)數(shù)據(jù)對(duì)象,就好比我們發(fā)email時(shí)帶的附件。obtainMessage用于從內(nèi)部的消息池中獲取一個(gè)消息,就像線程池對(duì)線程的復(fù)用一樣,通過這個(gè)方法獲取校區(qū)更加高效。獲取了消息并設(shè)置好它的what、obj后,我們?cè)诘?8行調(diào)用sendToTarget方法來發(fā)送消息。
下面我們來看看mMainHandler和threadPoolExecutor的創(chuàng)建代碼:
private static final int CORE_POOL_SIZE = CPU_COUNT + 1; //corePoolSize為CPU數(shù)加1 private static final int MAX_POOL_SIZE = 2 * CPU_COUNT + 1; //maxPoolSize為2倍的CPU數(shù)加1 private static final long KEEP_ALIVE = 5L; //存活時(shí)間為5s public static final Executor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); private Handler mMainHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { Result result = (Result) msg.what; ImageView imageView = result.imageView; String url = (String) imageView.getTag(IMG_URL); if (url.equals(result.url)) { imageView.setImageBitmap(result.bitmap); } else { Log.w(TAG, "The url associated with imageView has changed"); } }; };
從以上代碼中我們可以看到創(chuàng)建mMainHandler時(shí)使用了主線程的Looper,因此構(gòu)造mMainHandler的代碼可以放在子線程中執(zhí)行。另外,注意以上代碼中我們?cè)诮oimageView設(shè)置圖片時(shí)首先判斷了下它的url是否等于result中的url,若相等才顯示。我們知道ListView會(huì)對(duì)其中Item的View進(jìn)行復(fù)用,剛移出屏幕的Item的View會(huì)被即將顯示的Item所復(fù)用。那么考慮這樣一個(gè)場(chǎng)景:剛移出的Item的View中的圖片還在未加載完成,而這個(gè)View被新顯示的Item復(fù)用時(shí)圖片加載好了,那么圖片就會(huì)顯示在新Item處,這顯然不是我們想看到的。因此我們通過判斷imageView的url是否與剛加載完的圖片的url是否相等,并在
只有兩者相等時(shí)才顯示,就可以避免以上提到的情況。
2. 圖片的緩存
(1)緩存的創(chuàng)建
我們?cè)趫D片加載框架類(FreeImageLoader)的構(gòu)造方法中初始化LruCache和DiskLruCache,相關(guān)代碼如下:
private LruCache<String, Bitmap> mMemoryCache; private DiskLruCache mDiskLruCache; private ImageLoader(Context context) { mContext = context.getApplicationContext(); int maxMemory = (int) (Runtime.getRuntime().maxMemory() /1024); int cacheSize = maxMemory / 8; mMemorySize = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeof(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; File diskCacheDir = getAppCacheDir(mContext, "images"); if (!diskCacheDir.exists()) { diskCacheDir.mkdirs(); } if (diskCacheDir.getUsableSpace() > DISK_CACHE_SIZE) { //剩余空間大于我們指定的磁盤緩存大小 try { mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); } catch (IOException e) { e.printStackTrace(); } } }
(2)緩存的獲取與添加
內(nèi)存緩存的添加與獲取我們已經(jīng)介紹過,只需調(diào)用LruCache的put與get方法,示例代碼如下:
private void addToMemoryCache(String key, Bitmap bitmap) { if (getFromMemoryCache(key) == null) { //不存在時(shí)才添加 mMemoryCache.put(key, bitmap); } } private Bitmap getFromMemoryCache(String key) { return mMemoryCache.get(key); }
接下來我們看一下如何從磁盤緩存中獲取Bitmap:
private loadFromDiskCache(String url, int dstWidth, int dstHeight) throws IOException { if (Looper.myLooper() == Looper.getMainLooper()) { //當(dāng)前運(yùn)行在主線程,報(bào)錯(cuò) Log.w(TAG, "should not Bitmap in main thread"); } if (mDiskLruCache == null) { return null; } Bitmap bitmap = null; String key = getKeyFromUrl(url); DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot != null) { FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(0); FileDescriptor fileDescriptor = fileInputStream.getFD(); bitmap = decodeSampledBitmapFromFD(fileDescriptor, dstWidth, dstHeight); if (bitmap != null) { addToMemoryCache(key, bitmap); } } return bitmap; }
把Bitmap添加到磁盤緩存中的工作在loadFromNet方法中完成,當(dāng)從網(wǎng)絡(luò)上成功獲取圖片后,會(huì)把它存入磁盤緩存中。相關(guān)代碼如下:
private Bitmap loadFromNet(String url, int dstWidth, int dstHeight) throws IOException { if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException("Do not load Bitmap in main thread."); } if (mDiskLruCache == null) { return null; } String key = getKeyFromUrl(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (getStreamFromUrl(url, outputStream)) { editor.commit(); } else { editor.abort(); } mDiskLruCache.flush(); } return loadFromDiskCache(url, dstWidth, dstHeight); }
以上代碼的大概邏輯是:當(dāng)確認(rèn)當(dāng)前不在主線程并且mDiskLruCache不為空時(shí),從網(wǎng)絡(luò)上得到圖片并保存到磁盤緩存,然后從磁盤緩存中得到圖片并返回。
以上貼出的兩段代碼在最開頭都判斷了是否在主線程中,對(duì)于loadFromDiskCache方法來說,由于磁盤IO相對(duì)耗時(shí),不應(yīng)該在主線程中運(yùn)行,所以只會(huì)在日志輸出一個(gè)警告;而對(duì)于loadFromNet方法來說,由于在主線程中訪問網(wǎng)絡(luò)是不允許的,因此若發(fā)現(xiàn)在主線程,直接拋出一個(gè)異常,這樣做可以避免做了一堆準(zhǔn)備工作后才發(fā)現(xiàn)位于主線程中不能訪問網(wǎng)絡(luò)(即我們提早拋出了異常,防止做無用功)。
另外,我們?cè)谝陨蟽啥未a中都對(duì)mDiskLruCache是否為空進(jìn)行了判斷。這也是很必要的,設(shè)想我們做了一堆工作后發(fā)現(xiàn)磁盤緩存根本還沒有初始化,豈不是很冤枉。我們通過兩個(gè)if判斷可以盡量避免做無用功。
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)潔的圖片加載框架,下面我們來看看它的實(shí)際使用性能如何。
四、簡(jiǎn)單的性能測(cè)試
關(guān)于性能優(yōu)化的姿勢(shì),Android Developer已經(jīng)給出了最佳實(shí)踐方案,胡凱大神整理了官方的性能優(yōu)化典范,請(qǐng)見這里:Android性能專題。這里我們主要從內(nèi)存分配和圖片的平均加載時(shí)間這兩個(gè)方面來看一下我們的圖片加載框架是否能達(dá)到勉強(qiáng)可用的程度。完整的demo請(qǐng)見這里:FreeImageLoader
1. 內(nèi)存分配情況
運(yùn)行我們的demo,待圖片加載完全,我們用adb看一下我們的應(yīng)用的內(nèi)存分配情況,我這里得到的情況如下圖所示:
從上圖我們可以看到,Dalvik Heap分配的內(nèi)存為18003KB, Native Heap則分配了6212KB。下面我們來看一下FreeImageLoader平均每張圖片的加載時(shí)間。
2. 平均加載時(shí)間
這里我們獲取平均加載時(shí)間的方法非常直接,基本思想是如以下所示:
//加載圖片前的時(shí)間點(diǎn) long beforeTime = System.currentTimeMillis(); //加載圖片完成的時(shí)間點(diǎn) long afterTime = System.currentTimeMillis(); //total為圖片的總數(shù),averTime為加載每張圖片所需的平均時(shí)間 int averTime = (int) ((afterTime - beforeTime) / total)
然后我們維護(hù)一個(gè)計(jì)數(shù)值counts,每加載完一張就加1,當(dāng)counts為total時(shí)我們便調(diào)用一個(gè)回調(diào)方法onAfterLoad,在這個(gè)方法中獲取當(dāng)前時(shí)間點(diǎn)并計(jì)算平均加載時(shí)間。具體的代碼請(qǐng)看上面給出的demo地址。
我這里測(cè)試加載30張圖片時(shí),平均每張所需時(shí)間為1.265s。下面我們來用Universal Image Loader來加載這30張圖片,并與我們的FreeImageLoader比較一下。
3. 與UIL的比較
我這里用UIL加載圖片完成后,得到的內(nèi)存情況如下:
我們可以看到在,Native Heap的分配上,F(xiàn)reeImageLoader與UIL差不多;在Dalvik Heap分配上,UIL的大小快達(dá)到了FreeImageLoader的2倍。由于框架的量級(jí)不同,這說明不了FreeImageLoader在內(nèi)存占用上優(yōu)于UIL,但通過這個(gè)比較我們可以認(rèn)為我們剛剛實(shí)現(xiàn)的框架還是勉強(qiáng)可用的:)
我們?cè)賮砜匆幌耈IL的平均加載時(shí)間,我這里測(cè)試的結(jié)果是1.516ms,考慮到框架量級(jí)的差異,看來我們的框架在加載時(shí)間上還有提升空間。
五、更進(jìn)一步
經(jīng)過以上的步驟,我們可以看到,實(shí)現(xiàn)一個(gè)具有基本功能的圖片加載框架并不復(fù)雜,但我們可以做的還有更多:
現(xiàn)在的異步加載圖片方法需要顯式提供我們期望的圖片大小,一個(gè)實(shí)用的框架應(yīng)該能夠根據(jù)給定的ImageVIew自動(dòng)計(jì)算;
整個(gè)框架封裝在一個(gè)類中,模塊化方面顯然還可以做的更好;
不具備一個(gè)成熟的圖片加載框架應(yīng)該具有的各種功能...
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助。
相關(guān)文章
Android中RecyclerView實(shí)現(xiàn)多級(jí)折疊列表效果(二)
這篇文章主要給大家介紹了Android中RecyclerView實(shí)現(xiàn)多級(jí)折疊列表的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-05-05淺析Android App的相對(duì)布局RelativeLayout
這篇文章主要介紹了Android App的相對(duì)布局RelativeLayout,文中舉了一個(gè)登錄界面的XML布局例子,非常直觀,需要的朋友可以參考下2016-04-04android實(shí)現(xiàn)一鍵鎖屏和一鍵卸載的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于android如何實(shí)現(xiàn)一鍵鎖屏和一鍵卸載的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-05-05Android中AlertDialog四種對(duì)話框的最科學(xué)編寫用法(實(shí)例代碼)
這篇文章主要介紹了Android中AlertDialog四種對(duì)話框的最科學(xué)編寫用法,本文通過代碼講解的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11