【Android - 進階】之圖片三級緩存的原理及實現


  在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、實現

(1)網絡訪問工具類HttpUtil:
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;
    }
}
(2)操作文件系統的工具類FileUtil:
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;
    }
}
(3)LruCache的子類ImageCache:
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;
    }
}
(4)三級緩存的工具類CacheUtil:
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、調用

(1)MainActivity的布局文件activity_main.xml中的代碼:
<?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>
(2)MainActivity中的代碼:
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);
    }
}
(3)ListView的適配器類ImageAdapter中的代碼:
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);
    }
}
(4)ListView的Item的布局文件listitem_image.xml中的代碼:
<?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>
最終運行結果如下圖所示:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM