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);
                        }
                    });
                }
            });
        }
    }
);

当然,还有权限声明和动态权限申请,这些就不再给出。

效果就是点击后可以展示一个用户列表:

android RecyclerView 间距 android recycleview 缓存_缓存

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获取的当前元素的位于屏幕内,就从mAttachedScrapmCachedViews中再按照id来获取一次,获取到就返回;
  • 三级缓存,mViewCacheExtension,开发者自行实现的缓存,如果为null,就不进入;
  • 四级缓存,mRecyclerPool,本质上为SparseArray定义存储的ViewHolder

如果还没有获取到就创建一个新的ViewHolder

【注】也有人说是5级缓存,就是将第二部中的两个对象分开说,就是5级缓存。

通俗点来说,四级缓存:

  • 一级缓存,屏幕内缓存,直接通过下标位置从mChangedScrap列表中获取ViewHolder
  • 二级缓存,非屏幕内缓存,尝试从mAttachedScrap或者mCachedViews列表中获取ViewHolder,按照每项ItemId来进行获取;
  • 三级缓存,用户自定义实现的缓存,如果为null,就跳过;
  • 四级缓存,使用RecycledViewPool对象进行存储(在二级缓存如果获取成功,就会进行存储)。