在Android APP开发过程中有时候会用到列表来展示数据,如果只是展示很少的数据直接new一个list进行保存就可以了。如果在进行加载大量数据的时候只是单纯的加载到内存中,也就难免会出现卡顿现象。这次我就仿照别人的实现思路来完成内存的缓存和磁盘中的缓存实现RecycleVIew的加载很多的数据。
一、LruCache和DisLruCache简介
Lru全称是Least Recently Used也就是近期最少使用的意思,LruCache就是使用了近期最少使用算法,将最近不使用的对象从内存中释放掉。LruCache实现了内存中的缓存,而DisLruCache则是磁盘中的缓存。他们使用同样的算法,只是缓存地方不一样。下面来讲解我的实现过程,由于RecycleView不是重点,只有简单的几行代码,想要了解更多的相关只是,可自行google。
二、使用DiskLruCache实现数据在磁盘的缓存
由于我要实现在读取缓存的过程中如果读取不到数据我要到磁盘读取数据,因此我先进行磁盘数据的存储和读取的实现。这里使用DiskLruCache实现数据的磁盘缓存,也可以使用数据实现,我这里就不说使用数据缓存的实现了。
1、定义DiskLruCacheManage的磁盘管理类
/** * @param context * @param name 缓存路径的名字 */ public DiskLruCacheManage(Context context, String name) { this.context= context; this.name = name; openDiskCache(); }这里我需要在生命这个管理类的地方传一个上下文本,一个文件名进来初始化全局变量。openDiskCache()是本类中的一个重要方法,我要在这里进行创建一个缓存文件,将数据写入穿件好的缓存文件。实现如下
private void openDiskCache() { try { File cacheDir = getDiskCacheDir(context, name); if (!cacheDir.exists()) { cacheDir.mkdir(); } mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, DISK_CACHE_MAX_SIZE); } catch (IOException e) { e.printStackTrace(); } }这里主要是实现缓存文件的创建,然后使用DiskLruCache的方法打开文件。这里有必要说明一下DiskLruCache的open方法的参数,源码如下
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)这是一个静态方法,返回一个DiskLruCache对象,参数作如下介绍
- directory,就是在进行open之前创建的文件的路径
- appVersion,当前APP的版本号,如果APP的版本号有更新这会清空缓存文件
- valueCount,指定同一个key可以对应多少个缓存文件,一般都是传1,这样key和value一一对应
- maxSize,缓存文件的大小,单位是字节,所以一般定义的时候用M*1024Kb*1024B
2、保存数据功能的实现
经过DiskLruCache的open方法操作过后就有了操作文件的对象mDiskLruCache,然后就可以进行数据保存功能的实现public <T> void saveDataToDisk(T t, Class calss, final String key) { final String jsonStr = GsonFactory.getInstance().toJson(t, calss); Observable.empty() .subscribeOn(Schedulers.io()) .subscribe(new Consumer<Object>() { @Override public void accept(Object o) throws Exception { MyLog.i("*****************************1"); } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { MyLog.i("*****************************2"); } }, new Action() { @Override public void run() throws Exception { DiskLruCache.Editor editor; if (mDiskLruCache.isClosed()){ openDiskCache(); } editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream)); bw.write(jsonStr); editor.commit(); mDiskLruCache.flush(); bw.close(); mDiskLruCache.flush(); } }}); }此方法用于保存获取到的数据,为了是使文件保存工作不影响主线程的工作,我使用了RxJava2切换到io线程进行操作,至于RxJava2操作符的使用,可以参考 点击。
3、读取数据功能实现如下
public <T> T getDataFromDisk(String key, Class calss) { DiskLruCache.Snapshot snapshot = null; InputStreamReader reader = null; try { snapshot = mDiskLruCache.get(key); } catch (IOException e) { e.printStackTrace(); } try { if (snapshot != null) { StringWriter writer = new StringWriter(); InputStream inputStream = snapshot.getInputStream(0); reader = new InputStreamReader(inputStream, Charset.forName("UTF-8")); char[] buffer = new char[1024]; int count; while ((count = reader.read(buffer)) != -1) { writer.write(buffer, 0, count); } String jsonStr = writer.toString(); writer.close(); reader.close(); return (T) GsonFactory.getInstance().fromJson(jsonStr, calss); } } catch (IOException e) { e.printStackTrace(); } return null; }因为在保存的时候是使用key-value键值对进行保存的,在读取的时候也要根据key值才能获取到相应的数据。这里使用泛型来获取相应类的数据,如果你在使用过程中只在一个地方使用到,可以不使用泛型。我在使用的时候是存储的json字符串,在读取的时候就可以进行相应类型的转换。
4、数据存储文件的删除
在使用的过程中发现,有一个需求。就是在进行数据刷新的时候需要进行数据清除工作,于是我就添加了如下操作,如果你在使用的时候不需要刻意不进行添加此功能。
public void clear(){ try { mDiskLruCache.delete(); } catch (IOException e) { e.printStackTrace(); } }至此磁盘数据缓存整个模块写完了。
三、LruCache实现内存中的缓存
1、刷新界面的接口的定义
public interface UIUpdateCallBack { int getFirstVisablePosition(); int getLastVisablePosition(); void updataposition(int position,int itemCount); }
使用此接口实现界面的回调刷新
2、LruCache管理类的定义
public LruRecycleData(Context context, String specialName, UIUpdateCallBack callBack) { mItemCount = 0; testBeanDataCache = new LruCache<>(DATA_MAXSIZE); diskLruCacheManage = new DiskLruCacheManage(context, specialName); this.callBack = callBack; }
在构造函数的函数中进行变量的初始化,包括初始化LruCahce对象,磁盘数据缓存对象初始化、界面刷新接口的初始化。这个类中涉及到的全局变量有必要做一下说明
int DATA_MAXSIZE = 30; //缓存使用的内存大小,可以自行定义自己所需大小 int mPage = 30; //默认每页有多少条数的数据 private LruCache<Integer, TestBean.Data> testBeanDataCache = null; //定义LruCache变量来进行缓存数据 private int mItemCount; //当前获取到数据的总条数 private UIUpdateCallBack callBack; //刷新界面的回调接口 private ExecutorService executorService = Executors.newSingleThreadExecutor(); //线程池处理数据 private DiskLruCacheManage diskLruCacheManage;//磁盘数据缓存对象
3、数据缓存到内存中的实现
public void saveDataToCache(final TestBean bean, final int page) { if (testBeanDataCache == null || bean == null) return; executorService.submit(new Runnable() { @Override public void run() { if (page == 1) { //如果是第一页表示当前是进行数据刷新操作,要对所有的缓存数据进行清除 mItemCount = 0; testBeanDataCache.evictAll(); diskLruCacheManage.clear(); } int cacheLength = mItemCount; int size = bean.getData().size(); mPage = size == 0 ? 30 : bean.getData().size();//获取一次存储数据的条数 for (int i = 0; i < size; i++) { testBeanDataCache.put(cacheLength + i, bean.getData().get(i)); mItemCount = mItemCount + 1; } diskLruCacheManage.saveDataToDisk(bean, TestBean.class, page + "");//缓存到磁盘 if (page == 1) { callBack.updataposition(0, 0); } else { callBack.updataposition(cacheLength-2, size+2); } } }); }
在进行数据缓存到内存中的时候我这里有一个当前是第几页的判断,我想实现在第一页的进行缓存数据的清除工作,并重置一些全局变量。然后在我进行缓存的时候需要知道当前我应该从什么位置添加数据,就有了获取数据条数的操作。将数据缓存到内存到之后就需要实现将数据缓存到磁盘一份,然后进行界面的刷新操作。当然再行行刷新的时候我分两种情况进行刷新
- 如果当前是第一页,我都传0,然后在调用RecycleView的刷新API的时候是全部数据的刷新
- 如果不是第一页,传一个刷新的位置,然后就是从刷新的位置处开始刷新多少条数据
4、加载磁盘数据到内存缓存中
public void loadDataToCache(final int pos) { if (testBeanDataCache.get(pos) != null) return; executorService.submit(new Runnable() { @Override public void run() { int currentPage = pos / mPage + 1; TestBean bean = diskLruCacheManage.getDataFromDisk(currentPage + "", TestBean.class); if (bean != null) { //已经缓存到本地 int startPos = currentPage == 1 ? 0 : (currentPage - 1) * mPage - 1; //获取添加的位置 for (int i = startPos; i < startPos + bean.getData().size(); i++) { testBeanDataCache.put(i, bean.getData().get(i)); mItemCount = mItemCount + 1; } callBack.updataposition(pos,mPage); } } }); }
在RecycleView的Adapter中加载每条数据之前要进行判断当前数据是否在内存缓存中,如果不在我们要的这条数据不在内存中就要从磁盘中读取文件了。这里使用了线程池进行加载,如果你想了解线程池的东西可以自行Google。
在这里我首先根据当前位置以及每页多少条数据获取应该加载的页数,我在磁盘缓存的时候使用了page作为key进行保存了,然后就是读取数据,将数据插入到相应的位置。
5、完成RecycleView的Adapter中需要的方法
public int getItemCount() { //获取总数据的条目数 return mItemCount; } public TestBean.Data get(int position) {//获取当前位置的对象 return testBeanDataCache.get(position); }我添加了两个方法,一个获取已经获取数据的总条目数,一个是获取Adapter所需的当前位置的数据对象
四、RecycleView的Adapter设计
直接上代码
public class MainRecycleViewAdapter extends RecyclerView.Adapter<MainRecycleViewAdapter.ViewHolder> { private Context mContext; private LruRecycleData mRecycleData; public MainRecycleViewAdapter(Context context, LruRecycleData recycleData) { this.mContext = context; this.mRecycleData = recycleData; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = View.inflate(mContext, R.layout.item_main_rcv, null); ViewHolder viewHolder = new ViewHolder(view); return viewHolder; } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { mRecycleData.loadDataToCache(position);//加载当前数据 TestBean.Data bean = mRecycleData.get(position);//获取当前位置对象 if (bean == null) return; holder.textView.setText(bean.text); Glide.with(mContext) .load(bean.profile_image) .thumbnail((float) 0.1) .into(holder.logo_iv); } @Override public int getItemCount() { return mRecycleData == null ? 0 : mRecycleData.getItemCount();//获取总条目数 } @Override public void onViewAttachedToWindow(@NonNull ViewHolder holder) { mRecycleData.onViewAttachedToWindow(holder.getLayoutPosition()); super.onViewAttachedToWindow(holder); } class ViewHolder extends RecyclerView.ViewHolder { public ImageView logo_iv; public TextView textView; public ViewHolder(View itemView) { super(itemView); this.logo_iv = itemView.findViewById(R.id.logo_iv); this.textView = itemView.findViewById(R.id.content); } } }
以上是我在的Adapter的设计,在获取当前位置数据对象的时候需要去加载一次数据,如果当前位置数据对象不再内存中,就需要去从磁盘中获取
五、MainActivity中使用
public class MainActivity extends AppCompatActivity implements UIUpdateCallBack { private LinearLayoutManager mLayoutManager; private MainRecycleViewAdapter mRecycleViewAdapter; private LruRecycleData mLruRecycleData; private MDPullToRefresh mPullToRefresh; private int mPage = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mLruRecycleData = new LruRecycleData(this, "main", this); //实例化缓存对象 RecyclerView recyclerView = findViewById(R.id.main_recycleview); mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); recyclerView.setLayoutManager(mLayoutManager); recyclerView.addItemDecoration(new MainRVDividerDecoration(this)); mRecycleViewAdapter = new MainRecycleViewAdapter(this, mLruRecycleData);//创建Adapter recyclerView.setAdapter(mRecycleViewAdapter); mPullToRefresh = findViewById(R.id.refresh_view); mPullToRefresh.setOnRefreshListener(new OnRefreshListener() { @Override public void refreshListener() { mPage = 1; getData(1); } }); mPullToRefresh.setOnLoadMoreListener(new OnLoadMoreListener() { @Override public void loadMore() { mPage++; getData(mPage); } }); getData(mPage); } public void getData(final int page) { simple.getSampleData(1, page).subscribe(new ProgressSubscribe<>(this, new onSuccessListener<TestBean>() { @Override public void onSeccess(TestBean testBean) { if (testBean != null) { mLruRecycleData.saveDataToCache(testBean, page); //缓存数据到内存中 if (page ==1){ mPullToRefresh.refreshSuccess(); }else { mPullToRefresh.loadSuccess(); } } else { } } @Override public void onFailed(int code, String message) { } })); } @Override public int getFirstVisablePosition() { return mLayoutManager.findFirstVisibleItemPosition(); } @Override public int getLastVisablePosition() { return mLayoutManager.findLastVisibleItemPosition(); } @Override public void updataposition(final int position, final int itemCount) { runOnUiThread(new Runnable() { @Override public void run() { if (mRecycleViewAdapter != null) { if (position == 0) { //刷新Adapter中所有数据 mRecycleViewAdapter.notifyDataSetChanged(); } else {//刷新从position位置开始的itemcount条数据 mRecycleViewAdapter.notifyItemRangeChanged(position, itemCount); } } } }); } }至此,全部实现逻辑写完。