在前面的Bitmap文章中提到,Bitmap在使用中非常容易出现OOM,而本节主要介绍2个方法对加载多图/大图的情况进行优化,有效的避免OOM。
在使用RecyclerView、ListView等加载多图时,屏幕上显示的图片会通过滑动屏幕等事件不断地增加,最终导致OOM。为了保证内存始终维持在一个合理的范围,当item移除屏幕时要对图片进行回收,重新滚入屏幕时又要重新加载;Google官方推荐的是使用LruCache内存缓存技术,完美的解决了上面的问题。
内存缓存技术对大量占用程序应用内存的对象提供快速访问的方法。主要算法:把最近使用的对象用强引用存储在LinkedHashMap中,把最近最少使用的对象在缓存值达到限定时进行移除。
在使用LruCache缓存时应该考虑的因素:
使用案例:
使用程序内存的1/8作为缓存,向ImageView加载一张图片时,先从LruCache缓存中获取,为空则开启线程去加载;否则直接填充。
private LruCache 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(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 {
// 在后台加载图片。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}
第一部分中提到LruCache缓存技术实现了管理内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时。因此,Google又提出了一个新的方法,使用DiskLruCache对图片进行硬盘缓存。
现在我们大多数加载图片等用的都是第三方的框架如Glide等,会发现它们内部其实使用的也是DiskLruCache,它是如何使用的呢?
先从缓存的位置来说,它可以自定义缓存的路径,默认的路径为/sdcard/Android/data/
;默认路径的好处:
以包名为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//cache
cachePath = context.getExternalCacheDir().getPath();
} else {
// cachePath = /data/data//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()
将缓存数据全部删除
第一行 : 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文件的大小始终保持在一个合理的范围内。
案例:
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 {
private List mList;
private LruCache lruCache;
private DiskLruCache mDiskLruCache;
private Set tasks;
private RecyclerView rv;
private Context context;
private boolean isScrolling = false;
/**
* 记录每个子项的高度。
*/
private List hList;//定义一个List来存放图片的height
public Adapter(List 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(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 {
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 mList = gson.fromJson(Common.s, new TypeToken>() {
// }.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() {
// }.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