在Android開發中,如果圖片過多,而我們又沒有對圖片進行有效的緩存,就很容易導致OOM(Out Of Memory)錯誤。因此,圖片的緩存是非常重要的,尤其是對圖片非常多的應用。現在很多框架都做了很好的圖片緩存處理,如【Fresco】、【Glide】等。
本帖主要介紹以下Android中圖片的三級緩存機制的原理及其應用。本帖中的代碼都是使用Android原生的代碼編寫的。
1、原理
Android圖片三級緩存的原理如下圖所示:
可見,Android中圖片的三級緩存主要是強引用、軟銀用和文件系統。
Android原生為我們提供了一個LruCache,其中維護着一個LinkedHashMap。LruCache可以用來存儲各種類型的數據,但最常見的是存儲圖片(Bitmap)。LruCache創建LruCache時,我們需要設置它的大小,一般是系統最大存儲空間的八分之一。LruCache的機制是存儲最近、最后使用的圖片,如果LruCache中的圖片大小超過了其默認大小,則會將最老、最遠使用的圖片移除出去。
當圖片被LruCache移除的時候,我們需要手動將這張圖片添加到軟引用(SoftReference)中。我們需要在項目中維護一個由SoftReference組成的集合,其中存儲被LruCache移除出來的圖片。軟引用的一個好處是當系統空間緊張的時候,軟引用可以隨時銷毀,因此軟引用是不會影響系統運行的,換句話說,如果系統因為某個原因OOM了,那么這個原因肯定不是軟引用引起的。
下面敘述一下三級緩存的流程:
當我們的APP中想要加載某張圖片時,先去LruCache中尋找圖片,如果LruCache中有,則直接取出來使用,如果LruCache中沒有,則去SoftReference中尋找,如果SoftReference中有,則從SoftReference中取出圖片使用,同時將圖片重新放回到LruCache中,如果SoftReference中也沒有圖片,則去文件系統中尋找,如果有則取出來使用,同時將圖片添加到LruCache中,如果沒有,則連接網絡從網上下載圖片。圖片下載完成后,將圖片保存到文件系統中,然后放到LruCache中。
2、實現
import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; /** * 訪問Http的工具類 */ public class HttpUtil { private static HttpUtil instance; private HttpUtil() { } public static HttpUtil getInstance() { if (instance == null) { synchronized (HttpUtil.class) { if (instance == null) { instance = new HttpUtil(); } } } return instance; } /** * 通過path(URL)訪問網絡獲取返回的字節數組 */ public byte[] getByteArrayFromWeb(String path) { byte[] b = null; InputStream is = null; ByteArrayOutputStream baos = null; try { URL url = new URL(path); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setDoInput(true); connection.setConnectTimeout(5000); connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { baos = new ByteArrayOutputStream(); is = connection.getInputStream(); byte[] tmp = new byte[1024]; int length = 0; while ((length = is.read(tmp)) != -1) { baos.write(tmp, 0, length); } } b = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) { is.close(); } if (baos != null) { baos.close(); } } catch (Exception e) { e.printStackTrace(); } } return b; } }
import android.content.Context; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; /** * 操作內存文件的工具類 */ public class FileUtil { private static FileUtil instance; private Context context; private FileUtil(Context context) { this.context = context; } public static FileUtil getInstance(Context context) { if (instance == null) { synchronized (FileUtil.class) { if (instance == null) { instance = new FileUtil(context); } } } return instance; } /** * 將文件存儲到內存中 */ public void writeFileToStorage(String fileName, byte[] b) { FileOutputStream fos = null; try { File file = new File(context.getFilesDir(), fileName); fos = new FileOutputStream(file); fos.write(b, 0, b.length); } catch (Exception e) { e.printStackTrace(); } finally { try { if (fos != null) { fos.close(); } } catch (Exception e) { e.printStackTrace(); } } } /** * 從內存中讀取文件的字節碼 */ public byte[] readBytesFromStorage(String fileName) { byte[] b = null; FileInputStream fis = null; ByteArrayOutputStream baos = null; try { fis = context.openFileInput(fileName); baos = new ByteArrayOutputStream(); byte[] tmp = new byte[1024]; int len = 0; while ((len = fis.read(tmp)) != -1) { baos.write(tmp, 0, len); } b = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (fis != null) { fis.close(); } if (baos != null) { baos.close(); } } catch (Exception e) { e.printStackTrace(); } } return b; } }
import android.graphics.Bitmap; import android.os.Build; import android.support.annotation.RequiresApi; import android.util.LruCache; import java.lang.ref.SoftReference; import java.util.Map; /** * 圖片緩存 */ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR1) public class ImageCache extends LruCache<String, Bitmap> { private Map<String, SoftReference<Bitmap>> cacheMap; public ImageCache(Map<String, SoftReference<Bitmap>> cacheMap) { super((int) (Runtime.getRuntime().maxMemory() / 8)); this.cacheMap = cacheMap; } @Override // 獲取圖片大小 protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override // 當有圖片從LruCache中移除時,將其放進軟引用集合中 protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { if (oldValue != null) { SoftReference<Bitmap> softReference = new SoftReference<Bitmap>(oldValue); cacheMap.put(key, softReference); } } public Map<String, SoftReference<Bitmap>> getCacheMap() { return cacheMap; } }
import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.widget.ImageView; import java.io.File; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.Map; /** * 緩存工具類 */ public class CacheUtil { private static CacheUtil instance; private Context context; private ImageCache imageCache; private CacheUtil(Context context) { this.context = context; Map<String, SoftReference<Bitmap>> cacheMap = new HashMap<>(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { // SDK版本判斷 this.imageCache = new ImageCache(cacheMap); } } public static CacheUtil getInstance(Context context) { if (instance == null) { synchronized (CacheUtil.class) { if (instance == null) { instance = new CacheUtil(context); } } } return instance; } /** * 將圖片添加到緩存中 */ private void putBitmapIntoCache(String fileName, byte[] data) { // 將圖片的字節數組寫入到內存中 FileUtil.getInstance(context).writeFileToStorage(fileName, data); // 將圖片存入強引用(LruCache) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { imageCache.put(fileName, BitmapFactory.decodeByteArray(data, 0, data.length)); } } /** * 從緩存中取出圖片 */ private Bitmap getBitmapFromCache(String fileName) { // 從強引用(LruCache)中取出圖片 Bitmap bm = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) { // SDK版本判斷 bm = imageCache.get(fileName); if (bm == null) { // 如果圖片不存在強引用中,則去軟引用(SoftReference)中查找 Map<String, SoftReference<Bitmap>> cacheMap = imageCache.getCacheMap(); SoftReference<Bitmap> softReference = cacheMap.get(fileName); if (softReference != null) { bm = softReference.get(); imageCache.put(fileName, bm); } else { // 如果圖片不存在軟引用中,則去內存中找 byte[] data = FileUtil.getInstance(context).readBytesFromStorage(fileName); if (data != null && data.length > 0) { bm = BitmapFactory.decodeByteArray(data, 0, data.length); imageCache.put(fileName, bm); } } } } return bm; } /** * 使用三級緩存為ImageView設置圖片 */ public void setImageToView(final String path, final ImageView view) { final String fileName = path.substring(path.lastIndexOf(File.separator) + 1); Bitmap bm = getBitmapFromCache(fileName); if (bm != null) { view.setImageBitmap(bm); } else { // 從網絡獲取圖片 new Thread(new Runnable() { @Override public void run() { byte[] b = HttpUtil.getInstance().getByteArrayFromWeb(path); if (b != null && b.length > 0) { // 將圖片字節數組寫入到緩存中 putBitmapIntoCache(fileName, b); final Bitmap bm = BitmapFactory.decodeByteArray(b, 0, b.length); // 將從網絡獲取到的圖片設置給ImageView view.post(new Runnable() { @Override public void run() { view.setImageBitmap(bm); } }); } } }).start(); } } }
3、調用
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/id_main_lv_lv" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="#DDDDDD" android:dividerHeight="1.0dip" /> </RelativeLayout>
import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.widget.ListView; import com.example.itgungnir.testimagecache.R; import com.example.itgungnir.testimagecache.SharedData; import com.example.itgungnir.testimagecache.adapter.ImageAdapter; import java.util.Arrays; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView lv; private List<String> urlList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv = (ListView) findViewById(R.id.id_main_lv_lv); initData(); } // 初始化數據 private void initData() { // 初始化圖片URL列表 urlList = Arrays.asList(SharedData.IMAGE_URLS); } @Override protected void onResume() { super.onResume(); initView(); } // 初始化視圖 private void initView() { // 為ListView適配數據 ImageAdapter adapter = new ImageAdapter(MainActivity.this, urlList); lv.setAdapter(adapter); } }
import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.widget.ListView; import com.example.itgungnir.testimagecache.R; import com.example.itgungnir.testimagecache.SharedData; import com.example.itgungnir.testimagecache.adapter.ImageAdapter; import java.util.Arrays; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView lv; private List<String> urlList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv = (ListView) findViewById(R.id.id_main_lv_lv); initData(); } // 初始化數據 private void initData() { // 初始化圖片URL列表 urlList = Arrays.asList(SharedData.IMAGE_URLS); } @Override protected void onResume() { super.onResume(); initView(); } // 初始化視圖 private void initView() { // 為ListView適配數據 ImageAdapter adapter = new ImageAdapter(MainActivity.this, urlList); lv.setAdapter(adapter); } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="10.0dip"> <ImageView android:id="@+id/id_imageitem_image" android:layout_width="100.0dip" android:layout_height="100.0dip" android:layout_gravity="center_horizontal" android:contentDescription="@string/app_name" android:scaleType="fitXY" /> </LinearLayout>
