一起動(dòng)手編寫(xiě)Android圖片加載框架
開(kāi)發(fā)一個(gè)簡(jiǎn)潔而實(shí)用的Android圖片加載緩存框架,并在內(nèi)存占用與加載圖片所需時(shí)間這兩個(gè)方面與主流圖片加載框架之一Universal Image Loader做出比較,來(lái)幫助我們量化這個(gè)框架的性能。通過(guò)開(kāi)發(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í)際開(kāi)發(fā)工作之前,我們先來(lái)明確以下我們的需求。通常來(lái)說(shuō),一個(gè)實(shí)用的圖片加載框架應(yīng)該具備以下2個(gè)功能:
圖片的加載:包括從不同來(lái)源(網(wǎng)絡(luò)、文件系統(tǒng)、內(nèi)存等),支持同步及異步方式,支持對(duì)圖片的壓縮等等;
圖片的緩存:包括內(nèi)存緩存和磁盤(pán)緩存。
下面我們來(lái)具體描述下這些需求。
1. 圖片的加載
(1)同步加載與異步加載
我們先來(lái)簡(jiǎn)單的復(fù)習(xí)下同步與異步的概念:
同步:發(fā)出了一個(gè)“調(diào)用”后,需要等到該調(diào)用返回才能繼續(xù)執(zhí)行;
異步:發(fā)出了一個(gè)“調(diào)用”后,無(wú)需等待該調(diào)用返回就能繼續(xù)執(zhí)行。
同步加載就是我們發(fā)出加載圖片這個(gè)調(diào)用后,直到完成加載我們才繼續(xù)干別的活,否則就一直等著;異步加載也就是發(fā)出加載圖片這個(gè)調(diào)用后我們可以直接去干別的活。
(2)從不同的來(lái)源加載
我們的應(yīng)用有時(shí)候需要從網(wǎng)絡(luò)上加載圖片,有時(shí)候需要從磁盤(pán)加載,有時(shí)候又希望從內(nèi)存中直接獲取。因此一個(gè)合格的圖片加載框架應(yīng)該支持從不同的來(lái)源來(lái)加載一個(gè)圖片。對(duì)于網(wǎng)絡(luò)上的圖片,我們可以使用HttpURLConnection來(lái)下載并解析;對(duì)于磁盤(pán)中的圖片,我們可以使用BitmapFactory的decodeFile方法;對(duì)于內(nèi)存中的Bitmap,我們直接就可以獲取。
(3)圖片的壓縮
關(guān)于對(duì)圖片的壓縮,主要的工作是計(jì)算出inSampleSize,剩下的細(xì)節(jié)在下面實(shí)現(xiàn)部分我們會(huì)介紹。
2. 圖片的緩存
緩存功能對(duì)于一個(gè)圖片加載框架來(lái)說(shuō)是十分必要的,因?yàn)閺木W(wǎng)絡(luò)上加載圖片既耗時(shí)耗電又費(fèi)流量。通常我們希望把已經(jīng)加載過(guò)的圖片緩存在內(nèi)存或磁盤(pán)中,這樣當(dāng)我們?cè)俅涡枰虞d相同的圖片時(shí)可以直接從內(nèi)存緩存或磁盤(pán)緩存中獲取。
(1)內(nèi)存緩存
訪問(wèn)內(nèi)存的速度要比訪問(wèn)磁盤(pán)快得多,因此我們傾向于把更加常用的圖片直接緩存在內(nèi)存中,這樣加載速度更快,內(nèi)存緩存的不足在于由于內(nèi)存空間有限,能夠緩存的圖片也比較少。我們可以選擇使用SDK提供的LruCache類(lèi)來(lái)實(shí)現(xiàn)內(nèi)存緩存,這個(gè)類(lèi)使用了LRU算法來(lái)管理緩存對(duì)象,LRU算法即Least Recently Used(最近最少使用),它的主要思想是當(dāng)緩存空間已滿時(shí),移除最近最少使用的緩存對(duì)象。關(guān)于LruCache類(lèi)的具體使用我們下面會(huì)進(jìn)行詳細(xì)介紹。
(2)磁盤(pán)緩存
磁盤(pán)緩存的優(yōu)勢(shì)在于能夠緩存的圖片數(shù)量比較多,不足就是磁盤(pán)IO的速度比較慢。磁盤(pán)緩存我們可以用DiskLruCache來(lái)實(shí)現(xiàn),這個(gè)類(lèi)不屬于Android SDK,文末給出的本文示例代碼的地址,其中包含了DiskLruCache。
DisLruCache同樣使用了LRU算法來(lái)管理緩存,關(guān)于它的具體使用我們會(huì)在后文進(jìn)行介紹。
二、緩存類(lèi)使用介紹
1. LruCache的使用
首先我們來(lái)看一下LruCache類(lèi)的定義:
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è)泛型類(lèi),它的內(nèi)部使用一個(gè)LinkedHashMap來(lái)管理緩存對(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行我們重寫(xiě)了sizeOf方法,這個(gè)方法被LruCache用來(lái)計(jì)算一個(gè)緩存對(duì)象的大小。我們使用了getByteCount方法返回Bitmap對(duì)象以字節(jié)為單位的方法,又除以了1024,轉(zhuǎn)換為KB為單位的大小,以達(dá)到與cacheSize的單位統(tǒng)一。
(2)獲取緩存對(duì)象
LruCache類(lèi)通過(guò)get方法來(lái)獲取緩存對(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;
}
}
通過(guò)以上代碼我們了解到,首先會(huì)嘗試根據(jù)key獲取相應(yīng)value(第8行),若不存在則會(huì)新建一個(gè)key-value對(duì),并將它放入到LinkedHashMap中。從get方法的實(shí)現(xiàn)我們可以看到,它用synchronized關(guān)鍵字作了同步,因此這個(gè)方法是線程安全的。實(shí)際上,LruCache類(lèi)對(duì)所有可能涉及并發(fā)數(shù)據(jù)訪問(wèn)的方法都作了同步。
(3)添加緩存對(duì)象
在添加緩存對(duì)象之前,我們先得確定用什么作為被緩存的Bitmap對(duì)象的key,一種很直接的做法便是使用Bitmap的URL作為key,然而由于URL中存在一些特殊字符,所以可能會(huì)產(chǎn)生一些問(wèn)題。基于以上原因,我們可以考慮使用URL的md5值作為key,這能夠很好的保證不同的url具有不同的key,而且相同的url得到的key相同。我們自定義一個(gè)getKeyFromUrl方法來(lái)通過(guò)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原來(lái)對(duì)應(yīng)的value。
(4)刪除緩存對(duì)象
我們可以通過(guò)remove方法來(lái)刪除緩存對(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方法,如果有需要的話,我們可以重寫(xiě)entryRemoved方法來(lái)做一些資源回收的工作。
2. DiskLruCache的使用
(1)初始化DiskLruCache
通過(guò)查看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)造方法來(lái)創(chuàng)建DiskLruCache的實(shí)例。實(shí)際上DiskLruCache為我們提供了open靜態(tài)方法來(lái)創(chuàng)建一個(gè)DiskLruCache實(shí)例,我們來(lái)看一下這個(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ì)象的總大小超過(guò)了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是我們自定義的用來(lái)獲取磁盤(pán)緩存目錄的方法,它的定義如下:
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);
}
接下來(lái)我們介紹如何添加、獲取和刪除緩存對(duì)象。
(2)添加緩存對(duì)象
先通過(guò)以上介紹的getKeyFromUrl獲取Bitmap對(duì)象對(duì)應(yīng)的key,接下來(lái)我們就可以把這個(gè)Bitmap存入磁盤(pán)緩存中了。我們通過(guò)Editor來(lái)向DiskLruCache添加緩存對(duì)象。首先我們要通過(guò)edit方法獲取一個(gè)Editor對(duì)象:
String key = getKeyFromUrl(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key);
獲取到Editor對(duì)象后,通過(guò)調(diào)用Editor對(duì)象的newOutputStream我們就可以獲取key對(duì)應(yīng)的Bitmap的輸出流,需要注意的是,若我們想通過(guò)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è)輸出流中寫(xiě)入圖片數(shù)據(jù),成功寫(xiě)入后調(diào)用commit方法即可,若寫(xiě)入失敗則調(diào)用abort方法進(jìn)行回退。相關(guān)的代碼如下:
//getStream為我們自定義的方法,它通過(guò)URL獲取輸入流并寫(xiě)入outputStream,具體實(shí)現(xiàn)后文會(huì)給出
if (getStreamFromUrl(url, outputStream)) {
editor.commit();
} else {
//返回false表示寫(xiě)入outputStream未成功,因此調(diào)用abort方法回退整個(gè)操作
editor.abort();
}
mDiskLruCache.flush(); //將內(nèi)存中的操作記錄同步到日志文件中
下面我們來(lái)看一下getStream方法的實(shí)現(xiàn),這個(gè)方法實(shí)現(xiàn)很直接簡(jiǎn)單,就是創(chuàng)建一個(gè)HttpURLConnection,然后獲取InputStream再寫(xiě)入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è)自定義工具類(lèi)
HttpUtils.close(bis);
HttpUtils.close(bos);
}
return false;
}
經(jīng)過(guò)以上的步驟,我們已經(jīng)成功地將圖片寫(xiě)入了文件系統(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)部類(lèi),它包含一個(gè)getInputStream方法,通過(guò)這個(gè)方法可以獲取相應(yīng)緩存對(duì)象的輸入流,得到了這個(gè)輸入流,我們就可以進(jìn)一步獲取到Bitmap對(duì)象了。在獲取緩存的Bitmap時(shí),我們通常都要對(duì)它進(jìn)行一些預(yù)處理,主要就是通過(guò)設(shè)置inSampleSize來(lái)適當(dāng)?shù)目s放圖片,以防止出現(xiàn)OOM。我們之前已經(jīng)介紹過(guò)如何高效加載Bitmap,在那篇文章里我們的圖片來(lái)源于Resources。盡管現(xiàn)在我們的圖片來(lái)源是流對(duì)象,但是計(jì)算inSampleSize的方法是一樣的,只不過(guò)我們不再使用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來(lái)從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)見(jiàn)“Android開(kāi)發(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ò)的訪問(wèn),并且可能是耗時(shí)操作。同步加載的大致步驟如下:首先嘗試從內(nèi)存緩存中加載Bitmap,若不存在再?gòu)拇疟P(pán)緩存中加載,若不存在則從網(wǎng)絡(luò)中獲取并添加到磁盤(pán)緩存中。同步加載的代碼如下:
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;
}
//磁盤(pán)緩存中也不存在相應(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方法則是從磁盤(pán)緩存中獲取并縮放,而后返回縮放后的圖片。關(guān)于這兩個(gè)方法的實(shí)現(xiàn)在下面“圖片的緩存”部分我們會(huì)具體介紹。下面我們先來(lái)看看異步加載圖片的實(shí)現(xiàn)。
(2)異步加載
異步加載圖片在實(shí)際開(kāi)發(fā)中更經(jīng)常被使用,通常我們希望圖片加載框架幫我們?nèi)ゼ虞d圖片,我們接著干別的活,等到圖片加載好了,圖片加載框架會(huì)負(fù)責(zé)將它顯示在我們給定的ImageView中。我們可以使用線程池去執(zhí)行異步加載任務(wù),加載好后通過(guò)Handler來(lái)更新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是我們自定義的類(lèi),封裝了返回的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ì)通過(guò)發(fā)送消息給主線程來(lái)實(shí)現(xiàn)把Bitmap顯示在ImageView中。我們來(lái)簡(jiǎn)單的解釋下obtainMessage這個(gè)方法,我們傳入了兩個(gè)參數(shù),第一個(gè)參數(shù)代表消息的what屬性,這時(shí)個(gè)int值,相當(dāng)于我們給消息定的一個(gè)標(biāo)識(shí),來(lái)區(qū)分不同的消息;第二個(gè)參數(shù)代表消息的obj屬性,表示我們附帶的一個(gè)數(shù)據(jù)對(duì)象,就好比我們發(fā)email時(shí)帶的附件。obtainMessage用于從內(nèi)部的消息池中獲取一個(gè)消息,就像線程池對(duì)線程的復(fù)用一樣,通過(guò)這個(gè)方法獲取校區(qū)更加高效。獲取了消息并設(shè)置好它的what、obj后,我們?cè)诘?8行調(diào)用sendToTarget方法來(lái)發(fā)送消息。
下面我們來(lái)看看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處,這顯然不是我們想看到的。因此我們通過(guò)判斷imageView的url是否與剛加載完的圖片的url是否相等,并在
只有兩者相等時(shí)才顯示,就可以避免以上提到的情況。
2. 圖片的緩存
(1)緩存的創(chuàng)建
我們?cè)趫D片加載框架類(lèi)(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) {
//剩余空間大于我們指定的磁盤(pán)緩存大小
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2)緩存的獲取與添加
內(nèi)存緩存的添加與獲取我們已經(jīng)介紹過(guò),只需調(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);
}
接下來(lái)我們看一下如何從磁盤(pán)緩存中獲取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添加到磁盤(pán)緩存中的工作在loadFromNet方法中完成,當(dāng)從網(wǎng)絡(luò)上成功獲取圖片后,會(huì)把它存入磁盤(pán)緩存中。相關(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ò)上得到圖片并保存到磁盤(pán)緩存,然后從磁盤(pán)緩存中得到圖片并返回。
以上貼出的兩段代碼在最開(kāi)頭都判斷了是否在主線程中,對(duì)于loadFromDiskCache方法來(lái)說(shuō),由于磁盤(pán)IO相對(duì)耗時(shí),不應(yīng)該在主線程中運(yùn)行,所以只會(huì)在日志輸出一個(gè)警告;而對(duì)于loadFromNet方法來(lái)說(shuō),由于在主線程中訪問(wèn)網(wǎng)絡(luò)是不允許的,因此若發(fā)現(xiàn)在主線程,直接拋出一個(gè)異常,這樣做可以避免做了一堆準(zhǔn)備工作后才發(fā)現(xiàn)位于主線程中不能訪問(wèn)網(wǎng)絡(luò)(即我們提早拋出了異常,防止做無(wú)用功)。
另外,我們?cè)谝陨蟽啥未a中都對(duì)mDiskLruCache是否為空進(jìn)行了判斷。這也是很必要的,設(shè)想我們做了一堆工作后發(fā)現(xiàn)磁盤(pán)緩存根本還沒(méi)有初始化,豈不是很冤枉。我們通過(guò)兩個(gè)if判斷可以盡量避免做無(wú)用功。
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)潔的圖片加載框架,下面我們來(lái)看看它的實(shí)際使用性能如何。
四、簡(jiǎn)單的性能測(cè)試
關(guān)于性能優(yōu)化的姿勢(shì),Android Developer已經(jīng)給出了最佳實(shí)踐方案,胡凱大神整理了官方的性能優(yōu)化典范,請(qǐng)見(jiàn)這里:Android性能專(zhuān)題。這里我們主要從內(nèi)存分配和圖片的平均加載時(shí)間這兩個(gè)方面來(lái)看一下我們的圖片加載框架是否能達(dá)到勉強(qiáng)可用的程度。完整的demo請(qǐng)見(jiàn)這里:FreeImageLoader
1. 內(nèi)存分配情況
運(yùn)行我們的demo,待圖片加載完全,我們用adb看一下我們的應(yīng)用的內(nèi)存分配情況,我這里得到的情況如下圖所示:

從上圖我們可以看到,Dalvik Heap分配的內(nèi)存為18003KB, Native Heap則分配了6212KB。下面我們來(lái)看一下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。下面我們來(lái)用Universal Image Loader來(lái)加載這30張圖片,并與我們的FreeImageLoader比較一下。
3. 與UIL的比較
我這里用UIL加載圖片完成后,得到的內(nèi)存情況如下:

我們可以看到在,Native Heap的分配上,F(xiàn)reeImageLoader與UIL差不多;在Dalvik Heap分配上,UIL的大小快達(dá)到了FreeImageLoader的2倍。由于框架的量級(jí)不同,這說(shuō)明不了FreeImageLoader在內(nèi)存占用上優(yōu)于UIL,但通過(guò)這個(gè)比較我們可以認(rèn)為我們剛剛實(shí)現(xiàn)的框架還是勉強(qiáng)可用的:)
我們?cè)賮?lái)看一下UIL的平均加載時(shí)間,我這里測(cè)試的結(jié)果是1.516ms,考慮到框架量級(jí)的差異,看來(lái)我們的框架在加載時(shí)間上還有提升空間。
五、更進(jìn)一步
經(jīng)過(guò)以上的步驟,我們可以看到,實(shí)現(xiàn)一個(gè)具有基本功能的圖片加載框架并不復(fù)雜,但我們可以做的還有更多:
現(xiàn)在的異步加載圖片方法需要顯式提供我們期望的圖片大小,一個(gè)實(shí)用的框架應(yīng)該能夠根據(jù)給定的ImageVIew自動(dòng)計(jì)算;
整個(gè)框架封裝在一個(gè)類(lèi)中,模塊化方面顯然還可以做的更好;
不具備一個(gè)成熟的圖片加載框架應(yīng)該具有的各種功能...
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助。
相關(guān)文章
anroid開(kāi)發(fā)教程之spinner下拉列表的使用示例
這篇文章主要介紹了anroid的spinner下拉列表的使用示例,需要的朋友可以參考下2014-04-04
Kotlin基礎(chǔ)學(xué)習(xí)之lambda中return語(yǔ)句詳解
這篇文章主要給大家介紹了關(guān)于Kotlin基礎(chǔ)學(xué)習(xí)之lambda中return語(yǔ)句的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或使用Kotlin具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
Android SoundPool實(shí)現(xiàn)簡(jiǎn)短小音效
這篇文章主要為大家詳細(xì)介紹了Android SoundPool實(shí)現(xiàn)簡(jiǎn)短小音效,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11
解析Android開(kāi)發(fā)優(yōu)化之:軟引用與弱引用的應(yīng)用
Java從JDK1.2版本開(kāi)始,就把對(duì)象的引用分為四種級(jí)別,從而使程序能更加靈活的控制對(duì)象的生命周期。這四種級(jí)別由高到低依次為:強(qiáng)引用、軟引用、弱引用和虛引用,本篇文章重點(diǎn)介紹一下軟引用和弱引用2013-05-05
Android實(shí)現(xiàn)多點(diǎn)觸摸操作
這篇文章主要介紹了Android實(shí)現(xiàn)多點(diǎn)觸摸操作,實(shí)現(xiàn)圖片的放大、縮小和旋轉(zhuǎn)等處理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
Android控件Chronometer定時(shí)器的實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android控件Chronometer定時(shí)器的實(shí)現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Kotlin中的Checked Exception機(jī)制淺析
這篇文章主要給大家介紹了關(guān)于Kotlin中Checked Exception機(jī)制的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10

