1. RecycleView的基本用法
和ListView
类似,定义好Adapter
和对应的xml
布局文件,然后进行关联即可。唯一不同之处在于在RecycleView
中需要设置布局管理器。
1.1 后台接口
和前几篇文章类似,本次案例数据从后台SpringBoot
服务器加载,对应Controller
:
@RestController
public class RetrofitController {
@GetMapping(value="/test/1.0/users")
public Set<User> getUsers(){
System.out.println("R################");
return UserMap.getDataBaseUsers();
}
}
至于GET
请求的缓存相关的HTTP
请求头部,这里不设置,将在OkHttp
中使用拦截器来设置请求头部,达到缓存目的。这里使用UserMap
类来模拟数据库。
1.2 客户端
相关依赖:
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
implementation 'com.google.code.gson:gson:2.8.0'
对于xml
布局文件比较简单,这里不再给出。直接进入适配器Adapter
的部分:
public class RVUserAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context mContext;
private int mResId;
private List<User> mDatas;
public RVUserAdapter(Context context, int resId, List<User> datas) {
this.mContext = context;
this.mResId = resId;
this.mDatas = datas;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View root = LayoutInflater.from(this.mContext).inflate(this.mResId, parent, false);
return new MyViewHolder(root);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
MyViewHolder myViewHolder = (MyViewHolder) holder;
myViewHolder.left.setText(this.mDatas.get(position).getName());
myViewHolder.right.setText(this.mDatas.get(position).getUserID());
}
@Override
public int getItemCount() {
return this.mDatas.size();
}
static class MyViewHolder extends RecyclerView.ViewHolder{
private TextView left;
private TextView right;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
this.left = itemView.findViewById(R.id.item_left);
this.right = itemView.findViewById(R.id.item_right);
}
}
}
然后,使用OkHttp
框架,定义缓存所需要的拦截器:
/**
* 自定义拦截器。
* 目的:添加GET请求的本地缓存。
*/
class MyInteceptor implements Interceptor{
@Override
public Response intercept( Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.build();
// 响应请求头添加
//设置缓存时间为60秒,并移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
return chain.proceed(request).newBuilder()
.removeHeader("Pragma")
.addHeader("Cache-Control", "max-age=1800")
.addHeader("Last-Modified", getTime())
.build();
}
private String getTime(){
ZonedDateTime zonedDateTime = ZonedDateTime.now().with(LocalTime.MAX);
return zonedDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
}
因为OkHttpClient
的创建过程会创建很多对象,故而可以考虑让它唯一。故可以抽离出来OkHttpClient
的创建部分,如:
private Request initOkHttpClient(String url){
// 设置本地缓存的位置,在外部SD卡,所以需要动态申请相关权限
File file = new File(Environment.getExternalStorageDirectory(), "DCIM");
// 本地缓存容量大小设置为10MB
long cacheSize = 10 * 1024 * 1024L; // 10MB
if(client == null){
client = new OkHttpClient.Builder()
.addNetworkInterceptor(new MyInteceptor())
.cache(new Cache(file, cacheSize))
.build();
}
// 本地缓存过期时间设置为1分钟
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(1, TimeUnit.MINUTES)
.build();
return new Request.Builder()
.cacheControl(cacheControl)
.url(url)
.build();
}
其实最好是可以抽离为单例模式,这个案例就不抽离为单例模式了。最后就是点击按钮就进行网络请求,然后加载RecycleView
:
String url = "http://192.168.1.101:90/test/1.0/users";
Request request = initOkHttpClient(url);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
button.setText("数据请求失败。");
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String string = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
// gson解析
Gson gson = new Gson();
Log.d("TAG", string);
List<User> users = gson.fromJson(string, new TypeToken<List<User>>() {}.getType());
RVUserAdapter adapter = new RVUserAdapter(MainActivity.this, R.layout.listview_item, users);
LinearLayoutManager layoutManager = new LinearLayoutManager(MainActivity.this);
layoutManager.setOrientation(RecyclerView.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setAdapter(adapter);
}
});
}
});
}
}
);
当然,还有权限声明和动态权限申请,这些就不再给出。
效果就是点击后可以展示一个用户列表:
2. RecycleView的缓存机制
对于缓存,我们都知道对于ListView
或者RecycleView
的缓存指的是:对其子项的缓存机制,且而且区别在于:
-
ListView
有两级缓存,在屏幕与非屏幕内。 -
RecyclerView
有四级缓存,支持多个离屏ItemView
缓存(匹配pos
获取目标位置的缓存,如果匹配则无需再次bindView
),支持开发者自定义缓存处理逻辑,支持所有RecyclerView
共用同一个RecyclerViewPool
(缓存池)。
在RecycleView
中有很多内部类对象,而负责视图缓存的类主要为Recycler
:
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
*/
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder> mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
...
}
这个类的注释可以看出,Recycler
负责视图的复用,也就是缓存。在上面可以看出这个类中使用List
列表来对RecycleView
的子项进行缓存。下面来看下它的请求缓存的方法,即加载子项的方法:
// RecycleView -> Recycler.java
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
而tryGetViewHolderForPositionByDeadline
方法也就是主要方法了,这里就粘贴主要部分代码:
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
ViewHolder holder = null;
// 0) 如果它是改变的废弃的ViewHolder,在scrap的mChangedScrap找
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1)根据position分别在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2)根据id在scrap的mAttachedScrap、mCachedViews中查找
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
//3)在ViewCacheExtension中查找,一般不用到,所以没有缓存
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
//4)在RecycledViewPool中查找
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
//5)到最后如果还没有找到复用的ViewHolder,则新建一个
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
四级缓存分别为:
- 一级缓存,
mChangedScrap
,屏幕内缓存,通过下标位置从mChangedScrap
对象中获取ViewHolder对象; - 二级缓存,
mAttachedScrap
/mCachedViews
。尝试通过下标位置从mAttachedScrap
中获取ViewHolder
对象;如果获取不到,且通过mAdapterHelper
获取的当前元素的位于屏幕内,就从mAttachedScrap
和mCachedViews
中再按照id
来获取一次,获取到就返回; - 三级缓存,
mViewCacheExtension
,开发者自行实现的缓存,如果为null
,就不进入; - 四级缓存,
mRecyclerPool
,本质上为SparseArray
定义存储的ViewHolder
;
如果还没有获取到就创建一个新的ViewHolder
。
【注】也有人说是5
级缓存,就是将第二部中的两个对象分开说,就是5
级缓存。
通俗点来说,四级缓存:
- 一级缓存,屏幕内缓存,直接通过下标位置从
mChangedScrap
列表中获取ViewHolder
; - 二级缓存,非屏幕内缓存,尝试从
mAttachedScrap
或者mCachedViews
列表中获取ViewHolder
,按照每项ItemId
来进行获取; - 三级缓存,用户自定义实现的缓存,如果为
null
,就跳过; - 四级缓存,使用
RecycledViewPool
对象进行存储(在二级缓存如果获取成功,就会进行存储)。