这10张图拿去,别再说学不会RecyclerView的缓存复用机制了

news2024/11/23 4:54:10

ViewPager2是在RecyclerView的基础上构建而成的,意味着其可以复用RecyclerView对象的绝大部分特性,比如缓存复用机制等。

作为ViewPager2系列的第一篇,本篇的主要目的是快速普及必要的前置知识,而内容的核心,正是前面所提到的RecyclerView的缓存复用机制。


RecyclerView,顾名思义,它会回收其列表项视图以供重用

具体而言,当一个列表项被移出屏幕后,RecyclerView并不会销毁其视图,而是会缓存起来,以提供给新进入屏幕的列表项重用,这种重用可以:

  • 避免重复创建不必要的视图

  • 避免重复执行昂贵的findViewById

从而达到的改善性能、提升应用响应能力、降低功耗的效果。而要了解其中的工作原理,我们还得回到RecyclerView是如何构建动态列表的这一步。

RecyclerView是如何构建动态列表的?

与RecyclerView构建动态列表相关联的几个重要类中,Adapter与ViewHolder负责配合使用,共同定义RecyclerView列表项数据的展示方式,其中:

  • ViewHolder是一个_包含列表项视图(itemView)的封装容器,同时也是RecyclerView缓存复用的主要对象_。

  • Adapter则提供了_数据<->视图_ 的“绑定”关系,其包含以下几个关键方法:

    • onCreateViewHolder:负责创建并初始化ViewHolder及其关联的视图,但不会填充视图内容。
    • onBindViewHolder:负责提取适当的数据,填充ViewHolder的视图内容。

然而,这2个方法并非每一个进入屏幕的列表项都会回调,相反,由于视图创建及findViewById执行等动作都主要集中在这2个方法,每次都要回调的话反而效率不佳。因此,我们应该通过对ViewHolder对象积极地缓存复用,来尽量减少对这2个方法的回调频次。

  1. 最优情况是——取得的缓存对象正好是原先的ViewHolder对象,这种情况下既不需要重新创建该对象,也不需要重新绑定数据,即拿即用。

  2. 次优情况是——取得的缓存对象虽然不是原先的ViewHolder对象,但由于二者的列表项类型(itemType)相同,其关联的视图可以复用,因此只需要重新绑定数据即可。

  3. 最后实在没办法了,才需要执行这2个方法的回调,即创建新的ViewHolder对象并绑定数据。

实际上,这也是RecyclerView从缓存中查找最佳匹配ViewHolder对象时所遵循的优先级顺序。而真正负责执行这项查找工作的,则是RecyclerView类中一个被称为_回收者_的内部类——Recycler

Recycler是如何查找ViewHolder对象的?


    /**
     * ...
     * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
     * first level cache to find a matching View. If it cannot find a suitable View, Recycler will
     * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
     * {@link RecycledViewPool}.
     * 
     * 当调用getViewForPosition(int)方法时,Recycler会检查attached scrap和一级缓存(指的是mCachedViews)以找到匹配的View。 
     * 如果找不到合适的View,Recycler会先调用ViewCacheExtension的getViewForPositionAndType(RecyclerView.Recycler, int, int)方法,再检查RecycledViewPool对象。
     * ...
     */
    public abstract static class ViewCacheExtension {
        ...
    }

    public final class Recycler {
        ...
        /**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * 
         * 尝试通过从Recycler scrap缓存、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。
         * ...
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            if (mState.isPreLayout()) {
                // 0 尝试从mChangedScrap中获取ViewHolder对象
                holder = getChangedScrapViewForPosition(position);
                ...
            }
            if (holder == null) {
                // 1.1 尝试根据position从mAttachedScrap或mCachedViews中获取ViewHolder对象
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
            }
            if (holder == null) {
                ...
                final int type = mAdapter.getItemViewType(offsetPosition);
                if (mAdapter.hasStableIds()) {
                    // 1.2 尝试根据id从mAttachedScrap或mCachedViews中获取ViewHolder对象
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ...
                }
                if (holder == null && mViewCacheExtension != null) {
                    // 2 尝试从mViewCacheExtension中获取ViewHolder对象
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        ...
                    }
                }
                if (holder == null) { // fallback to pool
                    // 3 尝试从mRecycledViewPool中获取ViewHolder对象
                    holder = getRecycledViewPool().getRecycledView(type);
                    ...
                }
                if (holder == null) {
                    // 4.1 回调createViewHolder方法创建ViewHolder对象及其关联的视图
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ...
                }
            }
    
            if (mState.isPreLayout() && holder.isBound()) {
                ...
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                ...
                // 4.1 回调bindViewHolder方法提取数据填充ViewHolder的视图内容
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
    
            ...
    
            return holder;
        }
        ...
    }    

结合RecyclerView类中的源码及注释可知,Recycler会依次从mChangedScrap/mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool中尝试获取指定位置或ID的ViewHolder对象以供重用,如果全都获取不到则直接重新创建。这其中涉及的几层缓存结构分别是:

mChangedScrap/mAttachedScrap

mChangedScrap/mAttachedScrap主要用于_临时存放仍在当前屏幕可见、但被标记为「移除」或「重用」的列表项_,其均以ArrayList的形式持有着每个列表项的ViewHolder对象,大小无明确限制,但一般来讲,其最大数就是屏幕内总的可见列表项数。

    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

但问题来了,既然是当前屏幕可见的列表项,为什么还需要缓存呢?又是什么时候列表项会被标记为「移除」或「重用」的呢?

这2个缓存结构实际上更多是为了避免出现像_局部刷新_这一类的操作,导致所有的列表项都需要重绘的情形。

区别在于,mChangedScrap主要的使用场景是:

  1. 开启了列表项动画(itemAnimator),并且列表项动画的canReuseUpdatedViewHolder(ViewHolder viewHolder)方法返回false的前提下;
  2. 调用了notifyItemChanged、notifyItemRangeChanged这一类方法,通知列表项数据发生变化;
    boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
        return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
                viewHolder.getUnmodifiedPayloads());
    }
    
    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
            @NonNull List<Object> payloads) {
        return canReuseUpdatedViewHolder(viewHolder);
    }
        
    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) {
        return true;
    }

canReuseUpdatedViewHolder方法的返回值表示的不同含义如下:

  • true,表示可以重用原先的ViewHolder对象
  • false,表示应该创建该ViewHolder的副本,以便itemAnimator利用两者来实现动画效果(例如交叉淡入淡出效果)。

简单讲就是,mChangedScrap主要是为列表项数据发生变化时的动画效果服务的

mAttachedScrap应对的则是剩下的绝大部分场景,比如:

  • 像notifyItemMoved、notifyItemRemoved这种列表项发生移动,但列表项数据本身没有发生变化的场景。
  • 关闭了列表项动画,或者列表项动画的canReuseUpdatedViewHolder方法返回true,即允许重用原先的ViewHolder对象的场景。

下面以一个简单的notifyItemRemoved(int position)操作为例来演示:

notifyItemRemoved(int position)方法用于通知观察者,先前位于position的列表项已被移除, 其往后的列表项position都将往前移动1位。

为了简化问题、方便演示,我们的范例将会居于以下限制:

  • 列表项总个数没有铺满整个屏幕——意味着不会触发mCachedViews、mRecyclerPool等结构的缓存操作
  • 去除列表项动画——意味着调用notifyItemRemoved后RecyclerView只会重新布局子视图一次
  recyclerView.itemAnimator = null

理想情况下,调用notifyItemRemoved(int position)方法后,应只有位于position的列表项会被移除,其他的列表项,无论是位于position之前或之后,都最多只会调整position值,而不应发生视图的重新创建或数据的重新绑定,即不应该回调onCreateViewHolder与onBindViewHolder这2个方法。

为此,我们就需要将当前屏幕内的可见列表项暂时从当前屏幕剥离,临时缓存到mAttachedScrap这个结构中去。

等到RecyclerView重新开始布局显示其子视图后,再遍历mAttachedScrap找到对应position的ViewHolder对象进行复用。

mCachedViews

mCachedViews主要用于_存放已被移出屏幕、但有可能很快重新进入屏幕的列表项_。其同样是以ArrayList的形式持有着每个列表项的ViewHolder对象,默认大小限制为2。

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    int mViewCacheMax = DEFAULT_CACHE_SIZE;
    static final int DEFAULT_CACHE_SIZE = 2;

比如像朋友圈这种按更新时间的先后顺序展示的Feed流,我们经常会在快速滑动中确定是否有自己感兴趣的内容,当意识到刚才滑走的内容可能比较有趣时,我们往往就会将上一条内容重新滑回来查看。

这种场景下我们追求的自然是上一条内容展示的实时性与完整性,而不应让用户产生“才滑走那么一会儿又要重新加载”的抱怨,也即同样不应发生视图的重新创建或数据的重新绑定。

我们用几张流程示意图来演示这种情况:

同样为了简化问题、方便描述,我们的范例将会居于以下限制:

  • 关闭预拉取——意味着之后向上滑动时,都不会再预拉取「待进入屏幕区域」的一个列表项放入mCachedView了
recyclerView.layoutManager?.isItemPrefetchEnabled = false

  • 只存在一种类型的列表项,即所有列表项的itemType相同,默认都为0。

我们将图中的列表项分成了3块区域,分别是被滑出屏幕之外的区域、屏幕内的可见区域、随着滑动手势待进入屏幕的区域。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jMmvf6tM-1671025467278)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/415a594411e747da8ca838895e5ae280~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]

  1. 当position=0的列表项随着向上滑动的手势被移出屏幕后,由于mCachedViews初始容量为0,因此可直接放入;

  1. 当position=1的列表项同样被移出屏幕后,由于未达到mCachedViews的默认容量大小限制,因此也可继续放入;

  1. 此时改为向下滑动,position=1的列表项重新进入屏幕,Recycler就会依次从mAttachedScrap、mCachedViews查找可重用于此位置的ViewHolder对象;

  2. mAttachedScrap不是应对这种情况的,自然找不到。而mCachedViews会遍历自身持有的ViewHolder对象,对比ViewHolder对象的position值与待复用位置的position值是否一致,是的话就会将ViewHolder对象从mCachedViews中移除并返回;

  3. 此处拿到的ViewHolder对象即可直接复用,即符合前面所述的_最优情况_。

  1. 另外,随着position=1的列表项重新进入屏幕,position=7的列表项也会被移出屏幕,该位置的列表项同样会进入mCachedViews,即RecyclerView是双向缓存的。

mViewCacheExtension

mViewCacheExtension主要用于提供额外的、可由开发人员自由控制的缓存层级,属于非常规使用的情况,因此这里暂不展开讲。

mRecyclerPool

mRecyclerPool主要用于_按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项_,其会先以SparseArray区分不同的itemType,然后每种itemType对应的值又以ArrayList的形式持有着每个列表项的ViewHolder对象,每种itemType的ArrayList大小限制默认为5。

    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        ...
    }

由于mCachedViews默认的大小限制仅为2,因此,当滑出屏幕的列表项超过2个后,就会按照先进先出的顺序,依次将ViewHolder对象从mCachedViews移出,并按itemType放入RecycledViewPool中的不同ArrayList。

这种缓存结构主要考虑的是随着被滑出屏幕列表项的增多,以及被滑出距离的越来越远,重新进入屏幕内的可能性也随之降低。于是Recycler就在时间与空间上做了一个权衡,允许相同itemType的ViewHolder被提取复用,只需要重新绑定数据即可。

这样一来,既可以避免无限增长的ViewHolder对象缓存挤占了原本就紧张的内存空间,又可以减少回调相比较之下执行代价更加昂贵的onCreateViewHolder方法。

同样我们用几张流程示意图来演示这种情况,这些示意图将在前面的mCachedViews示意图基础上继续操作:

  1. 假设目前存在于mCachedViews中的仍是position=0及position=1这两个列表项。

  2. 当我们继续向上滑动时,position=2的列表项会尝试进入mCachedViews,由于超出了mCachedViews的容量限制,position=0的列表项会从mCachedViews中被移出,并放入RecycledViewPool中itemType为0的ArrayList,即图中的情况①;

  3. 同时,底部的一个新的列表项也将随着滑动手势进入到屏幕内,但由于此时mAttachedScrap、mCachedViews、mRecyclerPool均没有合适的ViewHolder对象可以提供给其复用,因此该列表项只能执行onCreateViewHolder与onBindViewHolder这2个方法的回调,即图中的情况②;

  1. 等到position=2的列表项被完全移出了屏幕后,也就顺利进入了mCachedViews中。

  1. 我们继续保持向上滑动的手势,此时,由于下一个待进入屏幕的列表项与position=0的列表项的itemType相同,因此我们可以在走到从mRecyclerPool查找合适的ViewHolder对象这一步时,根据itemType找到对应的ArrayList,再取出其中的1个ViewHolder对象进行复用,即图中的情况①。

  2. 由于itemType类型一致,其关联的视图可以复用,因此只需要重新绑定数据即可,即符合前面所述的_次优情况_。

  1. ②③ 情况与前面的一致,此处不再赘余。

最后总结一下,

RecyclerView缓存复用机制
对象ViewHolder(包含列表项视图(itemView)的封装容器)
目的减少对onCreateViewHolder、onBindViewHolder这2个方法的回调
好处1.避免重复创建不必要的视图 2.避免重复执行昂贵的findViewById
效果改善性能、提升应用响应能力、降低功耗
核心类Recycler、RecyclerViewPool
缓存结构mChangedScrap/mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool
缓存结构容器类型容量限制缓存用途优先级顺序(数值越小,优先级越高)
mChangedScrap/mAttachedScrapArrayList无,一般为屏幕内总的可见列表项数临时存放仍在当前屏幕可见、但被标记为「移除」或「重用」的列表项0
mCachedViewsArrayList默认为2存放已被移出屏幕、但有可能很快重新进入屏幕的列表项1
mViewCacheExtension开发者自己定义提供额外的可由开发人员自由控制的缓存层级2
mRecyclerPoolSparseArray每种itemType默认为5按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项3

以上的就是RecyclerView缓存复用机制的核心内容了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/90006.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQL概述以及MySQL常用语句总结

目录数据库概述SQL与NoSQL对比关系型数据库管理系统的常用实例MySQL介绍安装数据库的连接SQLDDLDMLDQL单表查询多表查询多表关系连接查询连接分类内连接 JOIN外连接左外连接 LEFT JOIN右外连接 RIGHT JOIN自连接 JOIN联合查询 UNION子查询标量子查询列子查询行子查询表子查询DC…

大二Web课程设计——家乡主题网页设计(web前端网页制作课作业) 四川旅游网页设计制作

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

【Spring】——12、BeanPostProcessor的执行流程?(源码分析)

&#x1f4eb;作者简介&#xff1a;zhz小白 公众号&#xff1a;小白的Java进阶之路 专业技能&#xff1a; 1、Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理 2、熟悉Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理&#xff0c;具备⼀定的线…

[附源码]Python计算机毕业设计房屋租赁管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等…

MongoDB Node 驱动使用指南

MongoDB Node 驱动介绍 1. MongoDB数据库连接指南 使用原生的mongodb Node驱动连接MongoDB数据库。 1.1 数据库连接URI 数据库连接URI是一个指明了数据库地址、名称、用户名、密码的字符串&#xff0c;类似于网页链接。 1.2 Node驱动安装 使用Npm或者Yarn安装数据库驱动程…

40_CAN通信基础知识

目录 CAN协议简介 CAN物理层 闭环总线网络 闭环总线网络 开环总线网络 通讯节点 差分信号 CAN协议中的差分信号 CAN协议层 CAN的波特率及位同步 位时序分解 SS段(SYNC SEG) PTS段(PROP SEG) PBS1段(PHASE SEG1) PBS2段(PHASE SEG2) 通讯的波特率 CAN的报文种类…

和数链技术与供应链金融的革命性融合发展

区块链是由密码技术、共识机制、点对点通信协议、分布式存储等多种核心技术体系高度融合&#xff0c;形成的一种分布式基础架构与计算范式&#xff0c;其本质则是一套去中心化的记账系统。区块链技术凭借自身分布式共享账本、去中心化、透明性、隐私保护、节点控制、信息的不可…

[附源码]计算机毕业设计的剧本杀管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

java+mysql基于ssm的校园快递代领系统

当代大学生的课业都比较繁重,很多学子甚至选修了双学位,但是为了保障生活的质量难免会在网上购买一些生活和学习用品,但是又因为学业或者兼职等原因不能按时的领取属于自己的快递,这个时候一个新兴的行业校园快递代领服务诞生了 本系统是一个校园快递代领系统,服务人员可以在线…

【Git】常用命令详解(循序渐进,逐步分析)

目录 一、Git常用命令 1.1、设置用户签名 1.2、初始化本地库 1.3查看本地库状态 1.3、添加文件到暂存区 1.4、将暂存区文件添加文件到本地库 1.5、查看历史版本&#xff08;提交历史记录&#xff09; 1.6、修改文件 1.7、版本穿梭 一、Git常用命令 1.1、设置用户签名…

前端浏览器支持的JS文件操作技术介绍

前端浏览器支持的JS文件操作技术介绍 本文将介绍前端浏览器支持的JS文件操作技术。通过使用在 HTML5 中加入到 DOM 的 File API&#xff0c;使在 web 内容中让用户选择本地文件然后读取这些文件的内容成为可能。用户可以通过 HTML 中的 <input type"file"> 元…

Python动态可视化Plotly

✨ 介绍 Plotly Express ✨&#xff1a; 提示&#xff1a;这里可以添加学习目标 Plotly Express是一个新的高级 Python 可视化库&#xff1a;它是Plotly.py的包装器&#xff0c;为复杂图表提供了简单的语法。受 Seaborn 和 ggplot2 的启发&#xff0c;它专门设计为具有简洁、…

【Meetup 明天见】OpenMLDB + MaxCompute:集成打通云上生态,高效构建 AI 应用

明天上午10&#xff1a;00-12:00&#xff0c;OpenMLDB 第八期 Meetup 将全程线上直播&#xff0c;欢迎关注。 活动背景 数据的爆发式增长为 AI 应用的繁荣提供了坚实的基础&#xff0c;而云服务作为新一代快速整合、高效计算的服务模式&#xff0c;为大数据的分析处理和 AI 智…

MySQL中SQL的执行流程

1 查询缓存 Server 如果在查询缓存中发现了这条 SQL 语句&#xff0c;就会直接将结果返回给客户端&#xff1b;如果没有&#xff0c;就进入到解析器阶段。需要说明的是&#xff0c;因为查询缓存往往效率不高&#xff0c;所以在 MySQL8.0 之后就抛弃了这个功能。 大多数情况查询…

Reconstructing Capsule Networks for Zero-shot Intent Classification

摘要 intent classification 意图分类。dialogue systems 对话系统已经存在的系统并没有能力去处理快速增长的意图。zero-shot intent classifcation&#xff1a; 零样本意图分类。 Nevertheless 不过。 incipient stage 初期阶段 今年来提出的IntentCapsNet two unaddresse…

基于java+springboot+mybatis+vue+mysql的智慧外贸平台

项目介绍 智慧外贸平台能够通过互联网得到广泛的、全面的宣传&#xff0c;让尽可能多的用户了解和熟知智慧外贸平台的便捷高效&#xff0c;不仅为群众提供了服务&#xff0c;而且也推广了自己&#xff0c;让更多的群众了解自己。对于智慧外贸而言&#xff0c;若拥有自己的系统…

播放量超1500w,谁的恰饭两次都在B站成顶流?

- 导语 女性消费一直以来都是消费市场的主力&#xff0c;“她经济”市场应运而生。有关数据显示&#xff0c;我国拥有近4亿消费者为女性&#xff0c;在如此庞大购买力的驱动下&#xff0c;截至目前统计我国共有492.9万家“她经济”相关企业&#xff0c;其中有3/4的是近5年内成…

【Spring】——16、使用@Autowired、@Qualifier、@Primary这三大注解自动装配组件?

&#x1f4eb;作者简介&#xff1a;zhz小白 公众号&#xff1a;小白的Java进阶之路 专业技能&#xff1a; 1、Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理 2、熟悉Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理&#xff0c;具备⼀定的线…

【Redis】Redis事务工作原理解析与分布式事务实战(Redis专栏启动)

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;专注于研究 Java/ Liunx内核/ C及汇编/计算机底层原理/源码&#xff0c;就职于大型金融公司后端高级工程师&#xff0c;擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。 &#x1…

8.Django大型电商项目之商品添加分页

1.添加分页 添加分页在Django中使用自带分页器paginator 1.1 配置setting settings中配置分页数量 # 每页显示记录条数 PER_PAGE_NUMBER 81.2 配置views 完成分页栏使用paginator创建对象&#xff0c;返回选中条数 from django.shortcuts import render from goodsapp.mo…