RecycleView数据缓存与显示性能的探究
Android开发中,我们经常会接触到需要使用RecycleView实现的需求;列表数目少的时候可以不考虑数据的缓存实现机制;但是当数据量很大的时候,比如可能有100万条记录,那么就需要考虑数据的缓存机制了;如果100万条记录都是放在List中,那么肯定会产生内存泄露的问题。
缓存的本质从技术上上来说是APP运行性能和资源占用的平衡。占用最少的机器资源达到最大的运行性能;
下面进入本文的主题。
首先从设计上来说,Activity显示一个RecycleView必须要有一个数据源,Activity作为控制端,RecycleView作为UI显示载体,数据源的作用是加载数据,缓存数据;使RecycleView显示时提供最快的页面显示效率并且防止内存泄露。
Activity与RecycleView系统本身做的已经够好了,我们只需要按照生命周期规范去重写相应的方法就可以满足要求;可是数据源这里系统只提供了一些基本的数据结构让我们来使用,缓存采取什么数据结构,缓存策略,如何加载等等问题需要我们自己去实现;而数据缓存实现的好坏是决定用户体验的。
我们需要达到的效果是页面加载块,内存占用低。因此我实现了一个Demo,简单的实现了2套缓存思路。代码如下:
首先定义好需要实现的接口:
//数据接口
public interface RecycleDataInf {
int BITMAP_MAXSIZE=8;
int DATA_MAXSIZE=30;
//和RecycleView主要方法对应
void onBindViewHolder(int position);
void onViewAttachedToWindow(int position);
void onViewDetachedFromWindow(int position);
void onViewRecycled(int position);
//获取总数目
int getItemCount();
//设置总数目
void setItemCount(int count);
//获取列表数据
TableVo get(int position);
//获取图片
Bitmap getBitmap(int position, String path);
//回收内存
void recycle();
}
UI加载接口
//与UI交互接口
public interface UiUpdateCallback {
int getFirstVisablePosition();
int getLastVisablePosition();
void updataposition(int position);
}
Adapter主要代码
public class Testadapter extends RecyclerView.Adapter {
private RecycleDataInf recycleList;
public Testadapter(RecycleDataInf recycleList) {
this.recycleList = recycleList;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new Holder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item, null));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
recycleList.onBindViewHolder(position);
if (holder instanceof Holder) {
final Holder h = (Holder) holder;
TableVo obj = recycleList.get(position);
h.view.setImageBitmap(null);
if (obj != null) {
Bitmap bitmap = recycleList.getBitmap(position, obj.getPath());
if (bitmap != null) {
h.view.setImageBitmap(bitmap);
}
}
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
recycleList.onViewAttachedToWindow(holder.getLayoutPosition());
super.onViewAttachedToWindow(holder);
}
@Override
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
recycleList.onViewDetachedFromWindow(holder.getLayoutPosition());
super.onViewDetachedFromWindow(holder);
}
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
recycleList.onViewRecycled(holder.getLayoutPosition());
super.onViewRecycled(holder);
}
@Override
public int getItemCount() {
return recycleList.getItemCount();
}
class Holder extends RecyclerView.ViewHolder {
ImageView view;
public Holder(View itemView) {
super(itemView);
view = (ImageView) itemView.findViewById(R.id.text);
}
}
}
Activity设置代码:
首先将模拟数据添加到数据库中,然后加载的时候从数据库读取
public class MainActivity extends Activity implements UiUpdateCallback {
RecyclerView pullToRefreshRecycleView;
private LinearLayoutManager linearLayoutManager;
private DbManager dbManage;
List<TableVo> tableVoList;
private RecycleDataInf recycleList;
private Testadapter testadapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbManage = new DbManager(this);
tableVoList = new ArrayList<>();
new Thread(new Runnable() {
@Override
public void run() {
int number = 0;
for (int i = 0; i < 1000; i++) {
TableVo vo = new TableVo();
vo.setTime(Calendar.getInstance().getTimeInMillis() + "");
vo.setTitle("第" + i + "个条目");
vo.setPath(getPath(i));
tableVoList.add(vo);
number++;
if (number == 100) {
dbManage.add(tableVoList);
tableVoList = new ArrayList<>();
number = 0;
}
}
tableVoList.clear();
runOnUiThread(new Runnable() {
@Override
public void run() {
loaddata();
}
});
}
}).start();
}
private void loaddata() {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
pullToRefreshRecycleView = (RecyclerView) findViewById(R.id.listview);
recycleList = new MapRecycleData(MainActivity.this, this);
recycleList.setItemCount(1000);
testadapter = new Testadapter(recycleList);
pullToRefreshRecycleView.setAdapter(testadapter);
linearLayoutManager = new LinearLayoutManager(MainActivity.this);
pullToRefreshRecycleView.setLayoutManager(linearLayoutManager);
}
@Override
public int getFirstVisablePosition() {
return linearLayoutManager.findFirstVisibleItemPosition();
}
@Override
public int getLastVisablePosition() {
return linearLayoutManager.findLastVisibleItemPosition();
}
@Override
public void updataposition(final int position) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (testadapter != null) {
testadapter.notifyItemChanged(position);
}
}
});
}
private String getPath(int position) {
return Environment.getExternalStorageDirectory() + File.separator + "Pictures" + File.separator + position + 1 + ".jpg";
}
@Override
protected void onDestroy() {
if (recycleList != null) {
recycleList.recycle();
}
recycleList = null;
super.onDestroy();
}
}
下面最主要的就是数据缓存具体策略实现了:
第一个我直接用LruCache来在内存中缓存数据,代码如下:
public class LruRecycleData implements RecycleDataInf {
/* 缓存数据的列**/
private LruCache<Integer, TableVo> dataCache = null;
/*缓存图片**/
private LruCache<Integer, Bitmap> bitmapcache = null;
/* 总数目**/
private int count = 0;
private UiUpdateCallback callback;
private ExecutorService service = Executors.newSingleThreadExecutor();
private ExecutorService bitmapservice = Executors.newSingleThreadExecutor();
private DbManager dbManager = null;
private Cursor cursor = null;
/* @param poolsize:需要缓存的数目**/
public LruRecycleData(Context context, UiUpdateCallback callback) {
this.callback = callback;
this.dataCache = new LruCache<>(DATA_MAXSIZE);
this.bitmapcache = new LruCache<>(BITMAP_MAXSIZE);
this.dbManager = new DbManager(context);
cursor = dbManager.get();
}
@Override
public void onBindViewHolder(int position) {
load(position);
}
@Override
public void onViewAttachedToWindow(int position) {
load(position);
}
@Override
public void onViewDetachedFromWindow(int position) {}
@Override
public void onViewRecycled(int position) {}
@Override
public int getItemCount() {
return count;
}
@Override
public void setItemCount(int count) {
this.count=count;
}
public TableVo get(int position) {
return dataCache.get(position);
}
@Override
public Bitmap getBitmap(int position, String path) {
Bitmap bitmap=bitmapcache.get(position);
if(bitmap==null){
loadBitmap(position,path);
}
return bitmap;
}
private void load(final int position) {
if (dataCache.get(position) != null) {
return;
}
service.submit(new Runnable() {
@Override
public void run() {
if (cursor != null && dataCache != null) {
cursor.moveToPosition(position);
TableVo vo = new TableVo();
vo.setPath(cursor.getString(cursor.getColumnIndex("url")));
vo.setTitle(cursor.getString(cursor.getColumnIndex("title")));
vo.setTime(cursor.getString(cursor.getColumnIndex("time")));
dataCache.put(position, vo);
callback.updataposition(position);
}
}
});
}
private void loadBitmap(final int position, final String path) {
if (dataCache.get(position) == null) {
return;
}
bitmapservice.submit(new Runnable() {
@Override
public void run() {
if (bitmapcache != null) {
Bitmap bitmap = bitmapcache.get(position);
if (bitmap == null) {
try {
bitmap = BitmapUtils.load(path);
bitmapcache.put(position, bitmap);
} catch (Exception e) {
e.printStackTrace();
}
}
if (bitmap != null) {
callback.updataposition(position);
}
}
}
});
}
public void recycle() {
if (cursor != null) {
cursor.close();
}
if (dbManager != null) {
dbManager = null;
}
cursor.close();
if (bitmapcache != null) {
bitmapcache.evictAll();
}
if (dataCache != null) {
dataCache.evictAll();
}
dataCache = null;
bitmapcache = null;
}
}
运行之后我用一个比较老的手机测试,列表滑动很流畅,图片加载速度还可以;毕竟是在本地加载;内存占用也很稳定,即使快速滑动,内存占用始终保持在一个水平线;不会发生内存泄露的情况。LruCachede缓存思路是当超过最多数量时,优先保留最近最经常使用的数据。
后面我自己实现了一个很简单的缓存策略,思路是只缓存当前显示数据的前后若干条数据。以当前显示条目为中心,缓存前后的一些数据。
代码如如下:
public class MapRecycleData implements RecycleDataInf { /* 缓存数据的列**/ private Map<Integer, TableVo> dataCache = null; /*缓存图片**/ private Map<Integer, Bitmap> bitmapcache = null; /* 总数目**/ private int count = 0; private int cacheMaxposition = 0; private UiUpdateCallback callback; private ExecutorService service = Executors.newSingleThreadExecutor(); private ExecutorService bitmapservice = Executors.newSingleThreadExecutor(); private DbManager dbManager = null; private Cursor cursor = null; /* @param poolsize:需要缓存的数目**/ public MapRecycleData(Context context, UiUpdateCallback callback) { this.callback = callback; this.dataCache = Collections.synchronizedMap(new LinkedHashMap<Integer, TableVo>()); this.bitmapcache = Collections.synchronizedMap(new LinkedHashMap<Integer, Bitmap>()); this.dbManager = new DbManager(context); cursor = dbManager.get(); } @Override public void onBindViewHolder(int position) { load(position); } @Override public void onViewAttachedToWindow(int position) { load(position); } @Override public void onViewDetachedFromWindow(int position) { } @Override public void onViewRecycled(int position) { int center = (callback.getFirstVisablePosition() + callback.getLastVisablePosition()) / 2; if (dataCache.size() > DATA_MAXSIZE) { for (int a = 0; a < center - DATA_MAXSIZE/2; a++) { dataCache.remove(a); } for (int a = cacheMaxposition; a > center + DATA_MAXSIZE/2; a--) { dataCache.remove(a); } } if (bitmapcache.size() > BITMAP_MAXSIZE) { for (int a = 0; a < center - BITMAP_MAXSIZE/2; a++) { bitmapcache.remove(a); } for (int a = cacheMaxposition; a > center + BITMAP_MAXSIZE/2; a--) { bitmapcache.remove(a); } } } @Override public int getItemCount() { return count; } public TableVo get(int position) { return dataCache.get(position); } @Override public Bitmap getBitmap(int position, String path) { Bitmap bitmap = bitmapcache.get(position); if (bitmap == null) { loadBitmap(position, path); } return bitmap; } private void load(final int position) { if (cacheMaxposition < position) { cacheMaxposition = position; } if (dataCache.get(position) != null) { return; } service.submit(new Runnable() { @Override public void run() { if (cursor != null && dataCache != null) { cursor.moveToPosition(position); TableVo vo = new TableVo(); vo.setPath(cursor.getString(cursor.getColumnIndex("url"))); vo.setTitle(cursor.getString(cursor.getColumnIndex("title"))); vo.setTime(cursor.getString(cursor.getColumnIndex("time"))); dataCache.put(position, vo); callback.updataposition(position); } } }); } @Override public void setItemCount(int count) { this.count = count; } private void loadBitmap(final int position, final String path) { if (dataCache.get(position) == null) { return; } bitmapservice.submit(new Runnable() { @Override public void run() { if (bitmapcache != null) { Bitmap bitmap = bitmapcache.get(position); if (bitmap == null) { try { bitmap = BitmapUtils.load(path); bitmapcache.put(position, bitmap); } catch (Exception e) { e.printStackTrace(); } } if (bitmap != null) { callback.updataposition(position); } } } }); } public void recycle() { if (cursor != null) { cursor.close(); } if (dbManager != null) { dbManager = null; } cursor.close(); if (bitmapcache != null) { bitmapcache.clear(); } if (dataCache != null) { dataCache.clear(); } dataCache = null; bitmapcache = null; } }
测试结果也不错。当然缓存策略实现的很简陋。没有LruCache实现的那么标准。不过2个缓存都实现了当你滑动的过程中,即使快速滑动,也不会出现下面加载很慢的情况。并且内存占用很稳定,不会出现内存泄露的情况。
以上测试仅仅是实现简单的思路而已,实际项目中缓存的设计还是比较复杂的;牵扯到网络,本地,内存等多方面的资源协调,还要结合具体机器的使用环境,机器本身各个硬件的性能等。不过不管实际如何复杂,设计缓存实现思路之前一定要有资源占用的大原则和缓存设计的方案以及讨论方案可能对实际性能造成的影响;不同的场景需要用到不同的方案。