Android 基礎之圖片載入(二)

weixin_34413065發表於2019-02-18

使用圖片快取技術

如何使用記憶體快取技術來對圖片進行快取,從而讓你的應用程式在載入很多圖片的時候可以提高響應速度和流暢性?
記憶體快取技術對那些大量佔用應用程式寶貴記憶體的圖片提供了快速訪問的方法。其中最核心的類是 LruCache (此類在 android-support-v4 的包中提供) 。這個類非常適合用來快取圖片,它的主要演算法原理是把最近使用的物件用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的物件在快取值達到預設定值之前從記憶體中移除。

使用 LruCache 來快取圖片的例項:

private LruCache<String, Bitmap> mMemoryCache;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    // 獲取到可用記憶體的最大值,使用記憶體超出這個值會引起OutOfMemory異常。
    // LruCache通過建構函式傳入快取值,以KB為單位。
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    // 使用最大可用記憶體值的1/8作為快取的大小。
    int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // 重寫此方法來衡量每張圖片的大小,預設返回圖片數量。
            return bitmap.getByteCount() / 1024;
        }
    };
}
 
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}
 
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

在這個例子當中,使用了系統分配給應用程式的八分之一記憶體來作為快取大小。在中高配置的手機當中,這大概會有 4M (32/8)的快取空間。一個全螢幕的 GridView 使用4張 800x480解析度的圖片來填充,則大概會佔用1.5兆的空間(8004804)。因此,這個快取大小可以儲存2.5頁的圖片。
當向 ImageView 中載入一張圖片時,首先會在 LruCache 的快取中進行檢查。如果找到了相應的鍵值,則會立刻更新ImageView ,否則開啟一個後臺執行緒來載入這張圖片。

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
    } else {
        imageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        task.execute(resId);
    }
}

BitmapWorkerTask 還要把新載入的圖片的鍵值對放到快取中。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    // 在後臺載入圖片。
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100);
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
}

DiskLruCache

為防止多圖 OOM 的核心解決思路就是使用 LruCache 技術。但 LruCache 只是管理了記憶體中圖片的儲存與釋放,如果圖片從記憶體中被移除的話,那麼又需要從網路上重新載入一次圖片,這顯然非常耗時。對此,Google 又提供了一套硬碟快取的解決方案:DiskLruCache (非Google官方編寫,但獲得官方認證)。

DiskLruCache 的用法

準備

由於DiskLruCache並不是由Google官方編寫的,所以這個類並沒有被包含在Android API當中,我們需要將這個類從網上下載下來,然後手動新增到專案當中。地址如下:
DiskLruCache.java 完整原始碼
下載好了原始碼之後,只需要在專案中新建一個libcore.io包,然後將DiskLruCache.java檔案複製到這個包中即可。

  1. DiskLruCache 是不能 new 出例項的,如果我們要建立一個 DiskLruCache 的例項,則需要呼叫它的 open() 方法,介面如下所示:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

open()方法接收四個引數,
第一個引數指定的是資料的快取地址。
第二個引數指定當前應用程式的版本號。
第三個引數指定同一個key可以對應多少個快取檔案,基本都是傳1。
第四個引數指定最多可以快取多少位元組的資料。

  1. 寫一個方法來獲取快取地址:
public File getDiskCacheDir(Context context, String uniqueName) {
    String cachePath;
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
            || !Environment.isExternalStorageRemovable()) {
        cachePath = context.getExternalCacheDir().getPath();
    } else {
        cachePath = context.getCacheDir().getPath();
    }
    return new File(cachePath + File.separator + uniqueName);
}
  1. 獲取到當前應用程式的版本號:
public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
}
  1. 因此,一個非常標準的open()方法就可以這樣寫:
DiskLruCache mDiskLruCache = null;
try {
    File cacheDir = getDiskCacheDir(context, "bitmap");
    if (!cacheDir.exists()) {
        cacheDir.mkdirs();
    }
    mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
    e.printStackTrace();
}
寫入快取

比如說現在有一張圖片,地址是https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg,那麼為了將這張圖片下載下來,就可以這樣寫:

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    HttpURLConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;
    try {
        final URL url = new URL(urlString);
        urlConnection = (HttpURLConnection) url.openConnection();
        in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
        out = new BufferedOutputStream(outputStream, 8 * 1024);
        int b;
        while ((b = in.read()) != -1) {
            out.write(b);
        }
        return true;
    } catch (final IOException e) {
        e.printStackTrace();
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
        try {
            if (out != null) {
                out.close();
            }
            if (in != null) {
                in.close();
            }
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }
    return false;
}

寫入的操作是藉助 DiskLruCache.Editor 這個類完成的。類似地,這個類也是不能 new 的,需要呼叫 DiskLruCache 的 edit() 方法來獲取例項,介面如下所示:

public Editor edit(String key) throws IOException

edit()方法接收一個引數key,這個key將會成為快取檔案的檔名,並且必須要和圖片的URL是一一對應的。那麼怎樣才能讓key和圖片的URL能夠一一對應呢?最簡單的做法就是將圖片的 URL 進行 MD5 編碼,編碼後的字串肯定是唯一的,並且只會包含 0-F 這樣的字元,完全符合檔案的命名規則。
寫一個方法用來將字串進行MD5編碼,程式碼如下

public String hashKeyForDisk(String key) {
    String cacheKey;
    try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(key.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(key.hashCode());
    }
    return cacheKey;
}
 
private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

現在,這樣寫就可以得到一個DiskLruCache.Editor的例項:


String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);

一次完整寫入操作的程式碼如下所示:

new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
            String key = hashKeyForDisk(imageUrl);
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream(0);
                if (downloadUrlToStream(imageUrl, outputStream)) {
                    editor.commit();
                } else {
                    editor.abort();
                }
            }
            mDiskLruCache.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();
讀取快取

讀取的方法要比寫入簡單一些,主要是藉助DiskLruCache的get()方法實現的,介面如下所示:

public synchronized Snapshot get(String key) throws IOException

完整的讀取快取,並將圖片載入到介面上的程式碼如下所示:

try {
    String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
    String key = hashKeyForDisk(imageUrl);
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    if (snapShot != null) {
        InputStream is = snapShot.getInputStream(0);
        Bitmap bitmap = BitmapFactory.decodeStream(is);
        mImage.setImageBitmap(bitmap);
    }
} catch (IOException e) {
    e.printStackTrace();
}
移除快取

移除快取主要是藉助DiskLruCache的remove()方法實現的,介面如下所示:

public synchronized boolean remove(String key) throws IOException

remove()方法中要求傳入一個key,然後會刪除這個key對應的快取圖片,示例程式碼如下:

try {
    String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
    String key = hashKeyForDisk(imageUrl);  
    mDiskLruCache.remove(key);
} catch (IOException e) {
    e.printStackTrace();
}

相關文章