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