[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 來說也已經夠用了