在前面的Bitmap文章中提到,Bitmap在使用中非常容易出現OOM,而本節主要介紹2個方法對加載多圖/大圖的情況進行優化,有效的避免OOM。
1.LruCache緩存
在使用RecyclerView、ListView等加載多圖時,螢幕上顯示的圖檔會通過滑動螢幕等事件不斷地增加,最終導緻OOM。為了保證記憶體始終維持在一個合理的範圍,當item移除螢幕時要對圖檔進行回收,重新滾入螢幕時又要重新加載;Google官方推薦的是使用LruCache記憶體緩存技術,完美的解決了上面的問題。
記憶體緩存技術對大量占用程式應用記憶體的對象提供快速通路的方法。主要算法:把最近使用的對象用強引用存儲在LinkedHashMap中,把最近最少使用的對象在緩存值達到限定時進行移除。
在使用LruCache緩存時應該考慮的因素:
- 裝置最大為每個程式配置設定的記憶體
- 圖檔被通路的頻率有多高,如存在個别通路頻率高的圖檔可以考慮使用多個LruCache來區分對象組
- 圖檔的尺寸大小,每張圖檔占用的記憶體大小
- 裝置的螢幕大小和分辨率
- 存儲多個低像素的圖檔,而在背景去開線程加載高像素的圖檔會更加的有效
使用案例:
使用程式記憶體的1/8作為緩存,向ImageView加載一張圖檔時,先從LruCache緩存中擷取,為空則開啟線程去加載;否則直接填充。
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 擷取到可用記憶體的最大值,使用記憶體超出這個值會引起OutOfMemory異常。
// LruCache通過構造函數傳入緩存值,以KB為機關。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用記憶體值的1/8作為緩存的大小。
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
}
/*
添加Bitmap到記憶體緩存中去
*/
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/*
記憶體緩存中擷取對應key的Bitmap
*/
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
// 從緩存中删除指定的Bitmap
public void removeBitmapFromMemory(String key) {
mMemoryCache.remove(key);
}
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
}
加載的異步任務:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
// 在背景加載圖檔。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}
2.DiskLruCache硬碟緩存
第一部分中提到LruCache緩存技術實作了管理記憶體中圖檔的存儲與釋放,如果圖檔從記憶體中被移除的話,那麼又需要從網絡上重新加載一次圖檔,這顯然非常耗時。是以,Google又提出了一個新的方法,使用DiskLruCache對圖檔進行硬碟緩存。
現在我們大多數加載圖檔等用的都是第三方的架構如Glide等,會發現它們内部其實使用的也是DiskLruCache,它是如何使用的呢?
先從緩存的位置來說,它可以自定義緩存的路徑,預設的路徑為
/sdcard/Android/data/<application package>/cache路徑
;預設路徑的好處:
- 存儲在SD卡上,不會對内置存儲造成影響
- 應用程式解除安裝後,相應的檔案也會被删除
以包名為com.wdl.card的APP為例,它的硬碟緩存的路徑為:/sdcard/Android/data/com.wdl.card/cache
使用DiskLruCache的标志:一個名為journal的檔案,它是DiskLruCache的一個日志檔案
使用簡介:工具類中包含擷取APP版本号/擷取緩存檔案位置等功能
package com.example.cachedemo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
/**
* 項目名: PhotoWallDemo
* 包名: com.example.photowalldemo
* 建立者: wdl
* 建立時間: 2019/2/18 14:39
* 描述: TODO
*/
public class Util {
public static File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
//SD卡存在且不可被移除時,調用getExternalCacheDir()擷取緩存位址
// cachePath = /sdcard/Android/data/<application package>/cache
cachePath = context.getExternalCacheDir().getPath();
} else {
// cachePath = /data/data/<application package>/cache
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
/**
* 網絡下載下傳并寫入檔案
*
* @param urlStr ip位址
* @param os OutputStream輸出流
* @return 是否寫入成功
*/
public static boolean downUrlToStream(final String urlStr, OutputStream os) {
HttpURLConnection urlConnection = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
final URL url = new URL(urlStr);
urlConnection = (HttpURLConnection) url.openConnection();
bis = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
bos = new BufferedOutputStream(os, 8 * 1024);
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (bos != null) {
bos.close();
}
if (bis != null) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 擷取加密key
*
* @param key 圖檔對應的url
* @return 加密後的url, 即為緩存檔案的名稱
*/
public static String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(key.getBytes());
cacheKey = bytesToHexString(messageDigest.digest());
} catch (Exception e) {
return String.valueOf(key.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* 擷取版本号
*
* @param context
* @return
*/
public static int getVersionCode(Context context) {
int versionCode = 0;
try {
versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return versionCode;
}
}
1.打開緩存 調用其open()方法
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
各個參數的含義如下表:
參數 | 含義 |
---|---|
directory | 資料的緩存位址–擷取緩存位址(考慮SD卡不存在或者SD卡剛剛被移除) |
appVersion | app版本号 |
valueCount | 一個Key對應的緩存檔案數 |
maxSize | 最多可以緩存多少位元組的資料 |
例子:省略抛出異常
DiskLruCache mDiskLruCache = null;
File cacheDir = getDiskCacheDir(context,'bitmap');
if(!cacheDir.exists()){
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir,getVersion(context),1,10*1024*1024);
獲得DiskLruCache執行個體後,就可以對其進行緩存的讀取/寫入/删除了
2.寫入
先擷取editor的執行個體後進行操作。
public Editor edit(String key) throws IOException key:代表緩存檔案的名稱且必須和圖檔url一一對應(利用MD5進行加密實作)
new Thread(new Runnable() {
@Override
public void run() {
try {
String url = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
//擷取緩存檔案名
String key = Util.hashKeyForDisk(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream os = editor.newOutputStream(0);
if (Util.downUrlToStream(url, os)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
擷取執行個體後可調用它的newOutputStream(inte index)擷取輸出流,然後将其傳入downloadUrlToStream中,即可實作下載下傳并寫入緩存目錄
index:由于前面在設定valueCount的時候指定的是1,是以這裡index傳0就可以了。
寫入後必須調用editor.commit()進行送出,調用abort()方法的話則表示放棄此次寫入。
3.讀取–借助DiskLruCache.get()方法
public synchronized Snapshot get(String key) throws IOException key:緩存檔案名 Snapshot為傳回值
利用 傳回值的getInputStream(int index)擷取輸入流,最後進行顯示
4.移除緩存–DiskLruCache.remove(String key)
public synchronized boolean remove(String key) throws IOException
5.常用API
size()
擷取緩存檔案的大小
flush()
将記憶體中的操作記錄同步至日志檔案(也就是journal檔案)。。比較标準的做法就是在Activity的onPause()方法中去調用一次flush()方法就可以了。
close()
關閉,通常是在destroy中調用,關閉後不可進行操作
delete()
将緩存資料全部删除
journal解讀
第一行 : libcore.io.DiskLruCache固定字元,标志我們使用DiskLruCache技術
第二行 : DiskLruCache的版本号,恒為1
第三行 : open時傳入的app版本号
第四行 : open時傳入的每個key對應的緩存檔案數
第五行 : 空行
第六行 : DIRTTY字首,後跟緩存圖檔的key; 每次調用DiskLruCache.edit(String key時)都會産生一條
後一行代表edit的結果,假如editor.commit()産生CLEAN key +該條緩存資料的大小,以位元組為機關;緩存成功;假如editor.abort()産生REMOVE key,寫入失敗,删除;
前面我們所學的size()方法可以擷取到目前緩存路徑下所有緩存資料的總位元組數,其實它的工作原理就是把journal檔案中所有CLEAN記錄的位元組數相加,求出的總合再把它傳回而已。
READ key 即調用DiskLruCache.get(String key)時産生的。
DiskLruCache中使用了一個redundantOpCount變量來記錄使用者操作的次數,每執行一次寫入、讀取或移除緩存的操作,這個變量值都會加1,當變量值達到2000的時候就會觸發重構journal的事件,這時會自動把journal中一些多餘的、不必要的記錄全部清除掉,保證journal檔案的大小始終保持在一個合理的範圍内。
3.DiskLruCache與LruCache結合實作照片牆
案例:
RecyclerView的設配器類:内部包含LruCache與DiskLruCache的使用;詳見注釋
package com.example.cachedemo;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.jakewharton.disklrucache.DiskLruCache;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* 建立時間: 2019/2/20 9:16
* 描述: TODO
*/
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
private List<String> mList;
private LruCache<String, Bitmap> lruCache;
private DiskLruCache mDiskLruCache;
private Set<Task> tasks;
private RecyclerView rv;
private Context context;
private boolean isScrolling = false;
/**
* 記錄每個子項的高度。
*/
private List<Integer> hList;//定義一個List來存放圖檔的height
public Adapter(List<String> mList, Context context, RecyclerView rv) {
this.mList = mList;
this.rv = rv;
this.context = context;
tasks = new HashSet<>();
hList = new ArrayList<>();
for (int i = 0; i < mList.size(); i++) {
//每次随機一個高度并添加到hList中
int height = new Random().nextInt(200) + 300;//[100,500)的随機數
hList.add(height);
}
//初始化記憶體緩存
int size = (int) ((Runtime.getRuntime().maxMemory() / 1024) / 8);
lruCache = new LruCache<String, Bitmap>(size) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
};
//初始化硬碟緩存
try {
File cacheDir = Util.getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists())
cacheDir.mkdirs();
mDiskLruCache = DiskLruCache.open(cacheDir,
Util.getVersionCode(context), 1, 30 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 建立viewHolder,将xml傳給viewholder
*
* @param parent
* @param viewType
* @return
*/
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
final String urlStr = mList.get(position);
//設定tag
viewHolder.imageView.setTag(urlStr);
//設定占位 防止圖檔錯位
viewHolder.imageView.setImageResource(R.drawable.ic_launcher_background);
//設定寬高
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(context
.getResources().getDisplayMetrics().widthPixels / 3, hList.get(position));
viewHolder.imageView.setLayoutParams(params);
loadBitmaps(viewHolder.imageView, urlStr);
}
public void setScrolling(boolean scrolling) {
isScrolling = scrolling;
notifyDataSetChanged();
}
/**
* 加載圖檔
*
* @param imageView ImageView
* @param urlStr 先從記憶體緩存中找,找不到,從硬碟緩存中找,找不到,網絡下載下傳并加載(存儲硬碟緩存),存儲到記憶體緩存
*/
private void loadBitmaps(ImageView imageView, String urlStr) {
Bitmap bitmap = getBitmapFromMemoryCache(urlStr);
//根據滑動的狀态判斷是否加載圖檔
if (bitmap == null&&!isScrolling) {
Task task = new Task();
tasks.add(task);
task.execute(urlStr);
} else {
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
@Override
public int getItemCount() {
return mList.size();
}
/**
* 添加記憶體緩存中不存在的Bitmap
*
* @param key 鍵
* @param bitmap 值
*/
private void addBitmapToLruCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null)
lruCache.put(key, bitmap);
}
/**
* 從記憶體緩存中擷取鍵為key的Bitmap
*
* @param key 鍵
* @return Bitmap
*/
private Bitmap getBitmapFromMemoryCache(String key) {
return lruCache.get(key);
}
/**
* 取消所有正在下載下傳或等待下載下傳的任務。
*/
public void cancelAllTasks() {
if (tasks != null) {
for (Task task : tasks) {
task.cancel(false);
}
}
}
/**
* 将緩存記錄同步到journal檔案中。
*/
public void flushCache() {
if (mDiskLruCache != null) {
try {
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.iv_image);
}
}
class Task extends AsyncTask<String, Void, Bitmap> {
private String imageUrl;
@Override
protected Bitmap doInBackground(String... strings) {
imageUrl = strings[0];
FileDescriptor fd = null;
FileInputStream fis = null;
DiskLruCache.Snapshot snapshot = null;
try {
//生成key
final String key = Util.hashKeyForDisk(imageUrl);
snapshot = mDiskLruCache.get(key);
//如果未找到緩存,則從網絡上下載下傳并存儲至緩存中
if (snapshot == null) {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream os = editor.newOutputStream(0);
if (Util.downUrlToStream(imageUrl, os)) {
editor.commit();
} else {
editor.abort();
}
}
//緩存被寫入後再次從緩存中查找
snapshot = mDiskLruCache.get(key);
}
if (snapshot != null) {
fis = (FileInputStream) snapshot.getInputStream(0);
fd = fis.getFD();
}
//将緩存資料加載成Bitmap
Bitmap bitmap = null;
if (fd != null) {
bitmap = BitmapFactory.decodeFileDescriptor(fd);
}
if (bitmap != null) {
//将bitmap寫入記憶體緩存中去
addBitmapToLruCache(imageUrl, bitmap);
}
return bitmap;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fd == null && fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
ImageView imageView = rv.findViewWithTag(imageUrl);
if (bitmap != null && imageView != null) {
imageView.setImageBitmap(bitmap);
}
tasks.remove(this);
}
}
}
MainActivity:使用
package com.example.cachedemo;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.Log;
import android.view.ViewTreeObserver;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
public class MainActivity extends AppCompatActivity {
private RecyclerView rv;
private Adapter adapter;
@Override
protected void onPause() {
super.onPause();
adapter.flushCache();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 退出程式時結束所有的下載下傳任務
adapter.cancelAllTasks();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rv = findViewById(R.id.recycler_view);
//設定瀑布流,2列豎直
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,
StaggeredGridLayoutManager.VERTICAL);
//解決item跳動
// layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
rv.setLayoutManager(layoutManager);
rv.setItemAnimator(null);
adapter = new Adapter(Arrays.asList(Common.urls),this,rv);
rv.setAdapter(adapter);
//添加recycler view 滾動狀态的監聽,控制是否加載圖檔
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState){
case SCROLL_STATE_IDLE:
//滑動停止
adapter.setScrolling(false);
break;
case SCROLL_STATE_DRAGGING:
//正在滾動
adapter.setScrolling(true);
break;
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
// Gson gson = new Gson();
// List<Entity> mList = gson.fromJson(Common.s, new TypeToken<List<Entity>>() {
// }.getType());
// StringBuilder str = new StringBuilder();
// for (Entity entity : mList) {
// str.append("\"").append(entity.getUrl()).append("\",").append("\n");
// }
// Log.e("wdl",str.toString());
// Entity[] entities = gson.fromJson(Common.s,new TypeToken<Entity>() {
// }.getType());
// for (Entity entity : entities) {
// str.append(entity.getUrl()).append("\n");
// }
// Log.e("wdl",str.toString());
}
}
效果展示:
onScrollStateChanged()回調方法的主要功能。
優化之一,在RecyclerView子項滾動時禁止加載圖檔,停止滑動時開始加載圖檔。
主要參考了郭神的文章,感謝大佬
案例下載下傳:https://download.csdn.net/download/qq_34341338/10975828