[Android]圖片資源三層儲存

前言

在 APP 中很多時候都要從網路中下載圖片

但如果是短時間內不會改變的圖片,那麼不需要再一次從網路上下載

可以利用手機本身的儲存方式來省略網路資源的浪費

那麼可以使用的方式就有二種

  • 記憶體儲存(LruCache)
  • 手機空間儲存(DiskLruCache)

若以上都沒有找到圖片資源的話,再從網路下載

記憶體儲存(LruCache)

跟 IO 和 Network 來比,記憶體是最快速的

所以會是我們的第一層儲存

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory /8;
LruCache lruCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        return bitmap.getByteCount() / 1024;
    }
};

實體化 LruCache 的時候要注意的部分有兩個

  • 儲存的空間大小

如果記憶體太大用光 APP 本身的記憶體就會有 OOM 的風險,如果太小又沒有意義

這邊使用的官方建議的數值 1/8 APP記憶體

  • 覆寫 sizeOf 方法回傳資源大小,單位為KB

接下來就和操作 MAP 一樣

lruCache.put(key, bitmap);
lruCache.get(key);
lruCache.remove(key);

手機空間儲存(DiskLruCache)

雖然記憶體儲存是最快的方式

不過一旦 APP 關閉後,記憶體也會被回收

所以在第二層會把檔案存到手機空間中

比起 LruCache 來說 DiskLruCache 操作會麻煩一些

這隻程式並沒有包含在原生程式中,要去官網下載

這邊要搭配幾個函式

//取得暫存目錄
public static 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);
}

//取得應用程式版本
public static int getAppVersion(Context context) {
    try {
        PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        return info.versionCode;
    } catch (PackageManager.NameNotFoundException e) {

    }

    return 1;
}
int diskCacheSize = 10 *1024 *1024; //10MB
File cacheDir = getDiskCacheDir(context, "bitmap");

if (!cacheDir.exists() && !cacheDir.mkdirs()){
    //do something
}

try {
    DiskLruCache diskLruCache = DiskLruCache.open(cacheDir, getAppVersion(this), 1, diskCacheSize);
} catch (IOException e) {
    //do something
}
  • 第三個參數代表的是 一個KEY可以對應幾組資源
  • 第四個參數代表的是 儲存單一檔案的容量上限
//取出資源
try {
    DiskLruCache.Snapshot snapShot = diskLruCache.get(key);

    if(snapShot != null){
        InputStream is = snapShot.getInputStream(0);
        Bitmap bitmap = BitmapFactory.decodeStream(is);
    }
} catch (IOException e) {
    //do something
}

//寫入資源
DiskLruCache.Editor editor = diskLruCache.edit(key);

if (editor != null){
    try{
        OutputStream cache = editor.newOutputStream(0);
        cache.write(); //寫入想要的資源
        cache.flush();
        cache.close();
        editor.commit();
    } catch (IOException e) {
        if (editor != null){
            try {
                editor.abort();
            } catch (IOException e1) {

            }
        }
    }
}

這邊要注意的部分是

snapShot.getInputStream(0);
editor.newOutputStream(0);

還記得初始化 DiskLruCache 時候的第三個參數嗎?

那個時候傳入的參數值是1

那麼上方那兩行的帶入的索引值就會是小於1

這個部分和陣列的意思是一樣的

然後官方建議在 onPause() 的時候才呼叫 DiskLruCache 的 flush()

並不需要每一次寫入資源的時候就呼叫一次

實作

介紹完了兩種儲存方式,那就來一下實作

Layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="300dp"
        android:layout_height="300dp" />

    <Button
        android:text="Load Image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/imageView"
        android:layout_alignParentStart="true"
        android:layout_marginStart="33dp"
        android:layout_marginTop="31dp"
        android:id="@+id/button" />

</RelativeLayout>

permission

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

Activity

public class MainActivity extends AppCompatActivity {
    private Activity activity;
    private ImageView imageView;
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.activity = this;
        this.imageView = (ImageView)findViewById(R.id.imageView);
        this.button = (Button)findViewById(R.id.button);

        this.button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Shared.loadImage("https://www.google.com.tw/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png", imageView, activity);
            }
        });
    }

    @Override
    protected void onPause() {
        super.onPause();

        try {
            if (Shared.diskLruCache != null){
                Shared.diskLruCache.flush();
            }
        } catch (IOException e) {

        }
    }

    @Override
    protected void onDestroy(){
        try {
            Shared.diskLruCache.close();
        } catch (IOException e) {

        }
    }
}

Shared

public class Shared {
    public static LruCache lruCache;
    public static DiskLruCache diskLruCache;

    public static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    public static final int cacheSize = maxMemory /8;
    public static final int diskCacheSize = 10 *1024 *1024;

    public static boolean initCache(Context context){
        File cacheDir = getDiskCacheDir(context, "bitmap");

        if (!cacheDir.exists() && !cacheDir.mkdirs()) return false;

        try {
            diskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, diskCacheSize);

            lruCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getByteCount() / 1024;
                }
            };
        } catch (IOException e) {
            return false;
        }

        return true;
    }

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

    public static int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {

        }

        return 1;
    }

    public static void loadImage(final String url, final ImageView imageView, final Activity activity) {
        if (diskLruCache == null){
            initCache(activity);
        }

        final String key = md5(url);
        Bitmap bitmap = null;

        if (lruCache != null) {
            bitmap = (Bitmap) lruCache.get(key);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
        }

        if (diskLruCache != null) {
            try {
                DiskLruCache.Snapshot snapShot = diskLruCache.get(key);
                if (snapShot != null) {
                    InputStream is = snapShot.getInputStream(0);
                    bitmap = BitmapFactory.decodeStream(is);
                    imageView.setImageBitmap(bitmap);
                    return;
                }
            } catch (IOException e) {

            }
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                DiskLruCache.Editor editor = null;
                OutputStream cache = null;
                InputStream in = null;
                ByteArrayOutputStream dataStream = null;
                BufferedOutputStream out = null;

                try {
                    editor = diskLruCache.edit(key);
                    in = new BufferedInputStream(new URL(url).openStream(), 1024);
                    dataStream = new ByteArrayOutputStream();
                    out = new BufferedOutputStream(dataStream, 1024);

                    int n = 0;
                    byte[] buffer = new byte[1024];

                    if (editor != null){
                        cache = editor.newOutputStream(0);
                        while (-1 != (n = in.read(buffer))) {
                            cache.write(buffer, 0, n);
                            out.write(buffer, 0, n);
                        }
                        cache.flush();
                        cache.close();
                    } else{
                        while (-1 != (n = in.read(buffer))) {
                            out.write(buffer, 0, n);
                        }
                    }

                    out.flush();

                    in.close();
                    out.close();
                    dataStream.close();

                    editor.commit();

                    byte[] data = dataStream.toByteArray();
                    final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);

                    lruCache.put(key, bitmap);

                    activity.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            imageView.setImageBitmap(bitmap);
                        }
                    });
                } catch (IOException e) {
                    Log.e("Cache", e.getMessage());

                    if (editor != null){
                        try {
                            editor.abort();
                        } catch (IOException e1) {

                        }
                    }

                    if (cache != null){
                        try {
                            cache.close();
                        } catch (IOException e1) {

                        }
                    }

                    if (in != null){
                        try {
                            in.close();
                        } catch (IOException e1) {

                        }
                    }

                    if (out != null){
                        try {
                            out.close();
                        } catch (IOException e1) {

                        }
                    }

                    if (dataStream != null){
                        try {
                            dataStream.close();
                        } catch (IOException e1) {

                        }
                    }
                }
            }
        }).start();
    }

    public static String md5(String key){
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(key.getBytes());

            byte[] digest = md.digest();
            StringBuffer sb = new StringBuffer();
            
            for (byte b : digest) {
                sb.append(String.format("%02x", b & 0xff));
            }

            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }
}

結論

或許會有人認為已經有功能強大的套件可以用,為什麼還需要自己寫

但我的看法是:套件也是基礎的東西做起來的,開發者自己也要稍微了解一下基礎的運用

至少別人問到了圖片載入的一些問題,不至於什麼都答不出來

今天的範例雖然簡單,但對一些單純的 APP 來說也已經夠用了

Read more

[LeetCode] #12 Integer to Roman 解題

題目連結 題型解說 這是一題難度為普通的題目 需要設計一個方法,此方法會傳入一個整數 num 要求是把整數轉換成羅馬字母,轉換清單如下 I => 1 V => 5 X => 10 L => 50 C => 100 D => 500 M => 1000 但羅馬字母有一些特殊規則 4 並非 IIII 而是 IV,9 並非 VIIII 而是 IX 這規則同樣可以套用到 40 90 400 900 解題思路 既然知道特殊規則是一樣的,變得是使用的符號,那麼先從 num 取個位數開始 轉換完成後,把 num 除上 10,消除個位數,

By Michael

[LeetCode] #11 Container With Most Water 解題

題目連結 題型解說 這是一題難度為中等的題目 需要設計一個方法,此方法會傳入一個數字陣列 height 陣列中的元素代表每一個柱子的高度 現在需要計算出,該陣列中以某兩隻柱子為邊界,最多可以裝多少水 以範例來說 height = [1,8,6,2,5,4,8,3,7] 最多可以裝 7 * 7 = 49 單位的水 解題思路 計算面積就是底乘上高 底的計算方式為 「右邊柱子的 index」 減去 「左邊柱子的 index」 高就取最短的那一根柱子高度 拿題目給的例子來當範例 建立三個變數 result、left、right left、right 代表左右兩邊的 index result 代表目前最大容量,初始值 0 第一步,找出最短的柱子高度,

By Michael

[LeetCode] #941 Valid Mountain Array 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個數字陣列 arr 判斷陣列中的元素是不是由低到高再從高到低(山形)的排序,且不連續一個以上數字 比如說 [1,2,3,2] 就是一個山形陣列,但 [1,2,2,3,2] 不是,因為有兩個 2 [1,2,3,4,5] 和 [5,4,3,2,1] 也不算是山形陣列,前者只有往上沒有往下,後者相反 解題思路 準備一個數字變數(temp)和布林變數(asc),跑一次迴圈,有可能遇到如下狀況 1. 某個數字與前一個數字相同,這時候直接回傳 false

By Michael

[LeetCode] #944 Delete Columns to Make Sorted 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個字串陣列 strs 這個陣列中每個字串的長度都相同,字串內容都是小寫英文 需要檢查每個元素的第 N 個字元是不是由小至大排列,並回傳有幾個錯誤排列 比如傳入的陣列長這樣 ["cba","daf","ghi"] 取第一個字元 = cdg 取第二個字元 = bah 取第三個字元 = afi 其中第二組的結果(bah)並不是由小至大排列,故回傳 1 解題思路 這一題就用兩個迴圈各別把字元取出來,並比較是否比上一個字元大(Java 中的字元可以直接比較),如果不是就將結果+1 程式碼 Java class Solution { public int minDeletionSize(String[] strs) { int result = 0; for (int i = 0,

By Michael