欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一起動手編寫Android圖片加載框架

 更新時間:2021年08月18日 15:22:28   作者:absfree  
這篇文章主要和大家一起動手編寫Android圖片加載框架,從內(nèi)部原理到具體實現(xiàn)來詳細(xì)介紹如何開發(fā)一個簡潔而實用的Android圖片加載緩存框架,感興趣的小伙伴們可以參考一下

開發(fā)一個簡潔而實用的Android圖片加載緩存框架,并在內(nèi)存占用與加載圖片所需時間這兩個方面與主流圖片加載框架之一Universal Image Loader做出比較,來幫助我們量化這個框架的性能。通過開發(fā)這個框架,我們可以進一步深入了解Android中的Bitmap操作、LruCache、LruDiskCache,讓我們以后與Bitmap打交道能夠更加得心應(yīng)手。若對Bitmap的大小計算及inSampleSize計算還不太熟悉,請參考這里:高效加載Bitmap。由于個人水平有限,敘述中必然存在不準(zhǔn)確或是不清晰的地方,希望大家能夠指出,謝謝大家。

一、圖片加載框架需求描述

在著手進行實際開發(fā)工作之前,我們先來明確以下我們的需求。通常來說,一個實用的圖片加載框架應(yīng)該具備以下2個功能:

圖片的加載:包括從不同來源(網(wǎng)絡(luò)、文件系統(tǒng)、內(nèi)存等),支持同步及異步方式,支持對圖片的壓縮等等;
圖片的緩存:包括內(nèi)存緩存和磁盤緩存。

下面我們來具體描述下這些需求。

1. 圖片的加載

(1)同步加載與異步加載

我們先來簡單的復(fù)習(xí)下同步與異步的概念:

同步:發(fā)出了一個“調(diào)用”后,需要等到該調(diào)用返回才能繼續(xù)執(zhí)行;
異步:發(fā)出了一個“調(diào)用”后,無需等待該調(diào)用返回就能繼續(xù)執(zhí)行。

同步加載就是我們發(fā)出加載圖片這個調(diào)用后,直到完成加載我們才繼續(xù)干別的活,否則就一直等著;異步加載也就是發(fā)出加載圖片這個調(diào)用后我們可以直接去干別的活。

(2)從不同的來源加載

我們的應(yīng)用有時候需要從網(wǎng)絡(luò)上加載圖片,有時候需要從磁盤加載,有時候又希望從內(nèi)存中直接獲取。因此一個合格的圖片加載框架應(yīng)該支持從不同的來源來加載一個圖片。對于網(wǎng)絡(luò)上的圖片,我們可以使用HttpURLConnection來下載并解析;對于磁盤中的圖片,我們可以使用BitmapFactory的decodeFile方法;對于內(nèi)存中的Bitmap,我們直接就可以獲取。

(3)圖片的壓縮

關(guān)于對圖片的壓縮,主要的工作是計算出inSampleSize,剩下的細(xì)節(jié)在下面實現(xiàn)部分我們會介紹。 

2. 圖片的緩存

緩存功能對于一個圖片加載框架來說是十分必要的,因為從網(wǎng)絡(luò)上加載圖片既耗時耗電又費流量。通常我們希望把已經(jīng)加載過的圖片緩存在內(nèi)存或磁盤中,這樣當(dāng)我們再次需要加載相同的圖片時可以直接從內(nèi)存緩存或磁盤緩存中獲取。

(1)內(nèi)存緩存

訪問內(nèi)存的速度要比訪問磁盤快得多,因此我們傾向于把更加常用的圖片直接緩存在內(nèi)存中,這樣加載速度更快,內(nèi)存緩存的不足在于由于內(nèi)存空間有限,能夠緩存的圖片也比較少。我們可以選擇使用SDK提供的LruCache類來實現(xiàn)內(nèi)存緩存,這個類使用了LRU算法來管理緩存對象,LRU算法即Least Recently Used(最近最少使用),它的主要思想是當(dāng)緩存空間已滿時,移除最近最少使用的緩存對象。關(guān)于LruCache類的具體使用我們下面會進行詳細(xì)介紹。

(2)磁盤緩存

磁盤緩存的優(yōu)勢在于能夠緩存的圖片數(shù)量比較多,不足就是磁盤IO的速度比較慢。磁盤緩存我們可以用DiskLruCache來實現(xiàn),這個類不屬于Android SDK,文末給出的本文示例代碼的地址,其中包含了DiskLruCache。

DisLruCache同樣使用了LRU算法來管理緩存,關(guā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是個泛型類,它的內(nèi)部使用一個LinkedHashMap來管理緩存對象。

(1)初始化LruCache

初始化LruCache的慣用代碼如下所示:

//獲取當(dāng)前進程的可用內(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)建了一個LruCache實例,并指定它的maxSize為當(dāng)前進程可用內(nèi)存的1/8。我們使用String作為key,value自然是Bitmap。第6行到第8行我們重寫了sizeOf方法,這個方法被LruCache用來計算一個緩存對象的大小。我們使用了getByteCount方法返回Bitmap對象以字節(jié)為單位的方法,又除以了1024,轉(zhuǎn)換為KB為單位的大小,以達(dá)到與cacheSize的單位統(tǒng)一。

(2)獲取緩存對象

LruCache類通過get方法來獲取緩存對象,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;
    }
  }

通過以上代碼我們了解到,首先會嘗試根據(jù)key獲取相應(yīng)value(第8行),若不存在則會新建一個key-value對,并將它放入到LinkedHashMap中。從get方法的實現(xiàn)我們可以看到,它用synchronized關(guān)鍵字作了同步,因此這個方法是線程安全的。實際上,LruCache類對所有可能涉及并發(fā)數(shù)據(jù)訪問的方法都作了同步。

(3)添加緩存對象

在添加緩存對象之前,我們先得確定用什么作為被緩存的Bitmap對象的key,一種很直接的做法便是使用Bitmap的URL作為key,然而由于URL中存在一些特殊字符,所以可能會產(chǎn)生一些問題?;谝陨显?,我們可以考慮使用URL的md5值作為key,這能夠很好的保證不同的url具有不同的key,而且相同的url得到的key相同。我們自定義一個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中添加緩存對象,這個方法的源碼如下:

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

從以上代碼我們可以看到這個方法確實也作了同步,它將新的key-value對放入LinkedHashMap后會返回相應(yīng)key原來對應(yīng)的value。 

(4)刪除緩存對象

我們可以通過remove方法來刪除緩存對象,這個方法的源碼如下:

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

這個方法會從LinkedHashMap中移除指定key對應(yīng)的value并返回這個value,我們可以看到它的內(nèi)部還調(diào)用了entryRemoved方法,如果有需要的話,我們可以重寫entryRemoved方法來做一些資源回收的工作。

 2. DiskLruCache的使用

(1)初始化DiskLruCache

通過查看DiskLruCache的源碼我們可以發(fā)現(xiàn),DiskLruCache就存在如下一個私有構(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的實例。實際上DiskLruCache為我們提供了open靜態(tài)方法來創(chuàng)建一個DiskLruCache實例,我們來看一下這個方法的實現(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個參數(shù),這4個參數(shù)的含義分別如下:

directory:代表緩存文件在文件系統(tǒng)的存儲路徑;
appVersion:代表應(yīng)用版本號,通常設(shè)為1即可;
valueCount:代表LinkedHashMap中每個節(jié)點上的緩存對象數(shù)目,通常設(shè)為1即可;
maxSize:代表了緩存的總大小,若緩存對象的總大小超過了maxSize,DiskLruCache會自動刪去最近最少使用的一些緩存對象。

以下代碼展示了初始化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);
}

接下來我們介紹如何添加、獲取和刪除緩存對象。 

(2)添加緩存對象

先通過以上介紹的getKeyFromUrl獲取Bitmap對象對應(yīng)的key,接下來我們就可以把這個Bitmap存入磁盤緩存中了。我們通過Editor來向DiskLruCache添加緩存對象。首先我們要通過edit方法獲取一個Editor對象:

String key = getKeyFromUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key); 

獲取到Editor對象后,通過調(diào)用Editor對象的newOutputStream我們就可以獲取key對應(yīng)的Bitmap的輸出流,需要注意的是,若我們想通過edit方法獲取的那個緩存對象正在被“編輯”,那么edit方法會返回null。相關(guān)的代碼如下:

if (editor != null) {
  OutputStream outputStream = editor.newOutputStream(0); //參數(shù)為索引,由于我們創(chuàng)建時指定一個節(jié)點只有一個緩存對象,所以傳入0即可
}

獲取了輸出流后,我們就可以向這個輸出流中寫入圖片數(shù)據(jù),成功寫入后調(diào)用commit方法即可,若寫入失敗則調(diào)用abort方法進行回退。相關(guān)的代碼如下:

//getStream為我們自定義的方法,它通過URL獲取輸入流并寫入outputStream,具體實現(xiàn)后文會給出
if (getStreamFromUrl(url, outputStream)) {
  editor.commit();
} else {
  //返回false表示寫入outputStream未成功,因此調(diào)用abort方法回退整個操作
  editor.abort();
}
mDiskLruCache.flush(); //將內(nèi)存中的操作記錄同步到日志文件中

下面我們來看一下getStream方法的實現(xiàn),這個方法實現(xiàn)很直接簡單,就是創(chuàng)建一個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為一個自定義工具類
    HttpUtils.close(bis);
    HttpUtils.close(bos);
  }
  return false;
}

經(jīng)過以上的步驟,我們已經(jīng)成功地將圖片寫入了文件系統(tǒng)。 

(3)獲取緩存對象

我們使用DiskLruCache的get方法從中獲取緩存對象,這個方法的大致源碼如下:

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

我們可以看到,這個方法最終返回了一個Snapshot對象,并以我們要獲取的緩存對象的key作為構(gòu)造參數(shù)之一。

Snapshot是DiskLruCache的內(nèi)部類,它包含一個getInputStream方法,通過這個方法可以獲取相應(yīng)緩存對象的輸入流,得到了這個輸入流,我們就可以進一步獲取到Bitmap對象了。在獲取緩存的Bitmap時,我們通常都要對它進行一些預(yù)處理,主要就是通過設(shè)置inSampleSize來適當(dāng)?shù)目s放圖片,以防止出現(xiàn)OOM。我們之前已經(jīng)介紹過如何高效加載Bitmap,在那篇文章里我們的圖片來源于Resources。盡管現(xiàn)在我們的圖片來源是流對象,但是計算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方法的實現(xiàn)請見“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)于這一方法的具體實現(xiàn)下文會進行介紹。

三、圖片加載框架的具體實現(xiàn)

1. 圖片的加載

(1)同步加載

同步加載的相關(guān)代碼需要在工作者線程中執(zhí)行,因為其中涉及到對網(wǎng)絡(luò)的訪問,并且可能是耗時操作。同步加載的大致步驟如下:首先嘗試從內(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對它進行縮放,返回縮放后的圖片。loadBitmapFromDisk方法則是從磁盤緩存中獲取并縮放,而后返回縮放后的圖片。關(guān)于這兩個方法的實現(xiàn)在下面“圖片的緩存”部分我們會具體介紹。下面我們先來看看異步加載圖片的實現(xiàn)。 

(2)異步加載

異步加載圖片在實際開發(fā)中更經(jīng)常被使用,通常我們希望圖片加載框架幫我們?nèi)ゼ虞d圖片,我們接著干別的活,等到圖片加載好了,圖片加載框架會負(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ū)別在于,異步加載把耗時任務(wù)放入了線程池中執(zhí)行。同步加載需要我們創(chuàng)建一個線程并在新線程中執(zhí)行l(wèi)oadBitmap方法,使用異步加載我們只需傳入url、imageView等參數(shù),圖片加載框架負(fù)責(zé)使用線程池在后臺執(zhí)行圖片加載任務(wù),加載成功后會通過發(fā)送消息給主線程來實現(xiàn)把Bitmap顯示在ImageView中。我們來簡單的解釋下obtainMessage這個方法,我們傳入了兩個參數(shù),第一個參數(shù)代表消息的what屬性,這時個int值,相當(dāng)于我們給消息定的一個標(biāo)識,來區(qū)分不同的消息;第二個參數(shù)代表消息的obj屬性,表示我們附帶的一個數(shù)據(jù)對象,就好比我們發(fā)email時帶的附件。obtainMessage用于從內(nèi)部的消息池中獲取一個消息,就像線程池對線程的復(fù)用一樣,通過這個方法獲取校區(qū)更加高效。獲取了消息并設(shè)置好它的what、obj后,我們在第18行調(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; //存活時間為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時使用了主線程的Looper,因此構(gòu)造mMainHandler的代碼可以放在子線程中執(zhí)行。另外,注意以上代碼中我們在給imageView設(shè)置圖片時首先判斷了下它的url是否等于result中的url,若相等才顯示。我們知道ListView會對其中Item的View進行復(fù)用,剛移出屏幕的Item的View會被即將顯示的Item所復(fù)用。那么考慮這樣一個場景:剛移出的Item的View中的圖片還在未加載完成,而這個View被新顯示的Item復(fù)用時圖片加載好了,那么圖片就會顯示在新Item處,這顯然不是我們想看到的。因此我們通過判斷imageView的url是否與剛加載完的圖片的url是否相等,并在

只有兩者相等時才顯示,就可以避免以上提到的情況。

 2. 圖片的緩存

(1)緩存的創(chuàng)建

我們在圖片加載框架類(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) {
    //不存在時才添加
    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)前運行在主線程,報錯
    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ò)上成功獲取圖片后,會把它存入磁盤緩存中。相關(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不為空時,從網(wǎng)絡(luò)上得到圖片并保存到磁盤緩存,然后從磁盤緩存中得到圖片并返回。

以上貼出的兩段代碼在最開頭都判斷了是否在主線程中,對于loadFromDiskCache方法來說,由于磁盤IO相對耗時,不應(yīng)該在主線程中運行,所以只會在日志輸出一個警告;而對于loadFromNet方法來說,由于在主線程中訪問網(wǎng)絡(luò)是不允許的,因此若發(fā)現(xiàn)在主線程,直接拋出一個異常,這樣做可以避免做了一堆準(zhǔn)備工作后才發(fā)現(xiàn)位于主線程中不能訪問網(wǎng)絡(luò)(即我們提早拋出了異常,防止做無用功)。

另外,我們在以上兩段代碼中都對mDiskLruCache是否為空進行了判斷。這也是很必要的,設(shè)想我們做了一堆工作后發(fā)現(xiàn)磁盤緩存根本還沒有初始化,豈不是很冤枉。我們通過兩個if判斷可以盡量避免做無用功。

現(xiàn)在我們已經(jīng)實現(xiàn)了一個簡潔的圖片加載框架,下面我們來看看它的實際使用性能如何。 

四、簡單的性能測試

關(guān)于性能優(yōu)化的姿勢,Android Developer已經(jīng)給出了最佳實踐方案,胡凱大神整理了官方的性能優(yōu)化典范,請見這里:Android性能專題。這里我們主要從內(nèi)存分配和圖片的平均加載時間這兩個方面來看一下我們的圖片加載框架是否能達(dá)到勉強可用的程度。完整的demo請見這里:FreeImageLoader

1. 內(nèi)存分配情況

運行我們的demo,待圖片加載完全,我們用adb看一下我們的應(yīng)用的內(nèi)存分配情況,我這里得到的情況如下圖所示:

從上圖我們可以看到,Dalvik Heap分配的內(nèi)存為18003KB, Native Heap則分配了6212KB。下面我們來看一下FreeImageLoader平均每張圖片的加載時間。

2. 平均加載時間

這里我們獲取平均加載時間的方法非常直接,基本思想是如以下所示:

//加載圖片前的時間點
long beforeTime = System.currentTimeMillis();
//加載圖片完成的時間點
long afterTime = System.currentTimeMillis();
//total為圖片的總數(shù),averTime為加載每張圖片所需的平均時間
int averTime = (int) ((afterTime - beforeTime) / total)

然后我們維護一個計數(shù)值counts,每加載完一張就加1,當(dāng)counts為total時我們便調(diào)用一個回調(diào)方法onAfterLoad,在這個方法中獲取當(dāng)前時間點并計算平均加載時間。具體的代碼請看上面給出的demo地址。

我這里測試加載30張圖片時,平均每張所需時間為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倍。由于框架的量級不同,這說明不了FreeImageLoader在內(nèi)存占用上優(yōu)于UIL,但通過這個比較我們可以認(rèn)為我們剛剛實現(xiàn)的框架還是勉強可用的:)

我們再來看一下UIL的平均加載時間,我這里測試的結(jié)果是1.516ms,考慮到框架量級的差異,看來我們的框架在加載時間上還有提升空間。 

五、更進一步

經(jīng)過以上的步驟,我們可以看到,實現(xiàn)一個具有基本功能的圖片加載框架并不復(fù)雜,但我們可以做的還有更多:

現(xiàn)在的異步加載圖片方法需要顯式提供我們期望的圖片大小,一個實用的框架應(yīng)該能夠根據(jù)給定的ImageVIew自動計算;
整個框架封裝在一個類中,模塊化方面顯然還可以做的更好;
不具備一個成熟的圖片加載框架應(yīng)該具有的各種功能...

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助。

相關(guān)文章

  • Android中RecyclerView實現(xiàn)多級折疊列表效果(二)

    Android中RecyclerView實現(xiàn)多級折疊列表效果(二)

    這篇文章主要給大家介紹了Android中RecyclerView實現(xiàn)多級折疊列表的相關(guān)資料,文中介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。
    2017-05-05
  • 淺析Android App的相對布局RelativeLayout

    淺析Android App的相對布局RelativeLayout

    這篇文章主要介紹了Android App的相對布局RelativeLayout,文中舉了一個登錄界面的XML布局例子,非常直觀,需要的朋友可以參考下
    2016-04-04
  • 完美解決虛擬按鍵遮蓋底部視圖的問題

    完美解決虛擬按鍵遮蓋底部視圖的問題

    下面小編就為大家分享一篇完美解決虛擬按鍵遮蓋底部視圖的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-01-01
  • 詳解Android Bitmap的常用壓縮方式

    詳解Android Bitmap的常用壓縮方式

    這篇文章主要介紹了詳解Android Bitmap的常用壓縮方式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01
  • 淺談Android LruCache的緩存策略

    淺談Android LruCache的緩存策略

    這篇文章主要介紹了淺談Android LruCache的緩存策略,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-04-04
  • Android自定義水平漸變進度條

    Android自定義水平漸變進度條

    這篇文章主要為大家詳細(xì)介紹了Android自定義水平漸變進度條,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-09-09
  • android實現(xiàn)一鍵鎖屏和一鍵卸載的方法實例

    android實現(xiàn)一鍵鎖屏和一鍵卸載的方法實例

    這篇文章主要給大家介紹了關(guān)于android如何實現(xiàn)一鍵鎖屏和一鍵卸載的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-05-05
  • Android CameraX打開攝像頭預(yù)覽教程

    Android CameraX打開攝像頭預(yù)覽教程

    大家好,本篇文章主要講的是Android CameraX打開攝像頭預(yù)覽教程,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下
    2021-12-12
  • Android中AlertDialog四種對話框的最科學(xué)編寫用法(實例代碼)

    Android中AlertDialog四種對話框的最科學(xué)編寫用法(實例代碼)

    這篇文章主要介紹了Android中AlertDialog四種對話框的最科學(xué)編寫用法,本文通過代碼講解的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-11-11
  • Android極光推送處理message遇到的坑解決

    Android極光推送處理message遇到的坑解決

    這篇文章主要為大家介紹了Android極光推送處理message遇到的坑解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02

最新評論