iOS 和 Android:UITableView与RecycleView的重用机制比较
引言:iOS和Android各有自己的列表组件。众所周知,列表组件一直都是移动端各个端中,组件重用、内存优化的重点。今天就来分析下iOS和Android各自的重用机制。
Android:RecyclerView的缓存机制
先来熟悉下ViewHolder的几个状态
- isInvalid:表示当前
ViewHolder
是否已经失效。通常来说,在3种情况下会出现这种情况:1.调用了Adapter
的notifyDataSetChanged
方法;2. 手动调用RecyclerView
的invalidateItemDecorations
方法;3. 调用RecyclerView
的setAdapter
方法或者swapAdapter
方法。- isRemoved:表示当前的
ViewHolder
是否被移除。通常来说,数据源被移除了部分数据,然后调用Adapter
的notifyItemRemoved
方法。- isBound:表示当前
ViewHolder
是否已经调用了onBindViewHolder
。- isTmpDetached:表示当前的
ItemView
是否从RecyclerView
(即父View
)detach
掉。通常来说有两种情况下会出现这种情况:1.手动了RecyclerView
的detachView
相关方法;2. 在从mHideViews
里面获取ViewHolder
,会先detach
掉这个ViewHolder
关联的ItemView
。- isScrap:表示是否在
mAttachedScrap
或者mChangedScrap
数组里面,进而表示当前ViewHolder
是否被废弃。- isUpdated:表示当前
ViewHolder
是否已经更新。通常来说,在3种情况下会出现情况:1.isInvalid
方法存在的三种情况;2.调用了Adapter
的onBindViewHolder
方法;3. 调用了Adapter
的notifyItemChanged
方法
mAttachedScrap、mChangedScrap
mAttachedScrap
存储的是当前还在屏幕中的ViewHolder
,用不是人话来描述,就是是从屏幕上分离出来,但是又即将添加到屏幕上去的ViewHolder(什么鬼?)。
打个比方,RecyclerView
上下滑动,此时会重新调用LayoutManager
的onLayoutChildren
方法,屏幕上所有的ViewHolder
先scrap
掉,添加到mAttachedScrap
里面去,然后在重新布局每个ItemView
时,会从优先mAttachedScrap
里面获取。而mChangedScrap
存储的是数据被更新的ViewHolder,
比如说调用了Adapter
的notifyItemChanged
方法。个人理解,这两个缓存个虽然也算是缓存重用,但和我们平时接触的有区别(这话讲的有点扭捏),它并不是传统的删除对象后缓存起来待到需要新建对象时复用的机制。
我们通过RecyclerView的源码来了解一下这两个缓存机制的区别
void scrapView(View view) {final ViewHolder holder = getChildViewHolderInt(view);if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {throw new IllegalArgumentException("Called scrap view with an invalid view."+ " Invalid views cannot be reused from scrap, they should rebound from"+ " recycler pool." + exceptionLabel());}holder.setScrapContainer(this, false);mAttachedScrap.add(holder);} else {if (mChangedScrap == null) {mChangedScrap = new ArrayList<ViewHolder>();}holder.setScrapContainer(this, true);mChangedScrap.add(holder);}}
这个方法简单来说就是通过holder的tag来区分应该放入mAttachedScrap还是mChangedScrap。
当holder满足以下几个条件之一,会被放入mAttachedScrap
- 被同时标记为
remove
和invalid
- 完全没有被改变
- canReuseUpdatedViewHolder方法的返回值为true 。canReuseUpdatedViewHolder方法嵌套了好几层函数调用,这里不展开,简单来说holder的mItemAnimator为空,或者mItemAnimator的
canReuseUpdatedViewHolder
方法为true
当这些条件都不满足时,或者说holder的isUpdated
方法返回为true时(即调用Adapter
的notifyItemChanged
方法时),会放入到mChangedScrap
里面去
mCachedViews
可以理解为RecyclerView的一级缓存,默认大小为2,也就是缓存2个ViewHolder。
有两点需要注意:
- mCachedViews只能复用同一位置的ViewHolder,什么概念呢?比如屏幕上有3个ViewHolder,此时向下滑动,出现了第4个ViewHolder,而第1个ViewHolder被移出了屏幕。然后再向上滑动,第1个ViewHolder重新回到屏幕内,第1个ViewHolder会被mCachedViews中取出并复用。而如果你在第1个ViewHolder被移除屏幕后,继续向下滑动,出现第5个ViewHolder,新出现的ViewHolder并不会从mCachedViews中复用,因为位置不同。
- mCachedViews中复用的ViewHolder中的数据不会被清除,复用时不会重新跑onBindViewHolder方法(因为位置相同,可以理解为是同一个,所以也不需要刷新数据)。
来看下源码:
void recycleViewHolderInternal(ViewHolder holder) {
//方法有点长,前面的掠过,看核心部分
if (forceRecycle || holder.isRecyclable()) {if (mViewCacheMax > 0&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {// Retire oldest cached viewint cachedViewSize = mCachedViews.size();if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {recycleCachedViewAt(0);cachedViewSize--;}int targetCacheIndex = cachedViewSize;if (ALLOW_THREAD_GAP_WORK&& cachedViewSize > 0&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {// when adding the view, skip past most recently prefetched viewsint cacheIndex = cachedViewSize - 1;while (cacheIndex >= 0) {int cachedPos = mCachedViews.get(cacheIndex).mPosition;if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {break;}cacheIndex--;}targetCacheIndex = cacheIndex + 1;}mCachedViews.add(targetCacheIndex, holder);cached = true;}if (!cached) {addViewHolderToRecycledViewPool(holder, true);recycled = true;}
}
注意第二行的判断条件,前面一段分析了holder在什么tag情况下会被加入到mAttachedScrap、mChangedScrap。很明显,这里明确定义了holder的tag不符合加入mAttachedScrap、mChangedScrap的条件的情况下,才会被加入到mCachedViews
mRecyclerPool
RecyclerView的二级缓存,根据不同的 item type 创建不同的 List,每个 List 默认大小为5个(也就是复用5个ViewHolder)。不同于mCachedViews,mRecyclerPool没有位置要求,只有type要求。但是复用的ViewHolder中的数据会被清除,因此复用时,会重跑onBindViewHolder方法。
注意刚才贴出的源码中,有这么一段:
if (!cached) {addViewHolderToRecycledViewPool(holder, true);recycled = true;}
可以理解为,如果不满足加入mAttachedScrap、mChangedScrap的条件,而又没有加入到mCachedViews的情况下,holder启动加入到mRecyclerPool的流程。
再来看一下addViewHolderToRecycledViewPool的内容
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {clearNestedRecyclerViewIfNotNested(holder);if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);ViewCompat.setAccessibilityDelegate(holder.itemView, null);}if (dispatchRecycled) {dispatchViewRecycled(holder);}holder.mOwnerRecyclerView = null;getRecycledViewPool().putRecycledView(holder);}public void putRecycledView(ViewHolder scrap) {final int viewType = scrap.getItemViewType();final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {return;}if (DEBUG && scrapHeap.contains(scrap)) {throw new IllegalArgumentException("this scrap item already exists");}scrap.resetInternal();scrapHeap.add(scrap);}
注意getScrapDataForType这个方法,证明了之前的说法,mRecyclerPool是基于type运作的
mViewCacheExtension
自定义缓存,通常用不到
来看复用
RecyclerView复用的核心方法是
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs)
这个方法比较长,我们只贴部分代码出来。
首先,是关于mChangedScrap的:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
...
if (mState.isPreLayout()) {holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;}
...
}
其次是mAttachedScrap和mCachedViews
if (holder == null) {holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);...}if (mAdapter.hasStableIds()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
这两个方法从源码分析,都是先从其次是mAttachedScrap,然后再从mCachedViews里复用holder。从方法名可以看出,一个是基于位置复用,另一个是基于id复用
再者是mRecyclerPool
if (holder == null) { // fallback to poolif (DEBUG) {Log.d(TAG, "tryGetViewHolderForPositionByDeadline("+ position + ") fetching from shared pool");}holder = getRecycledViewPool().getRecycledView(type);if (holder != null) {holder.resetInternal();if (FORCE_INVALIDATE_DISPLAY_LIST) {invalidateDisplayListInt(holder);}}}
最后,如果完全没有holder可以复用,会调用mAdapter.createViewHolder来创建holder
if (holder == null) {long start = getNanoTime();if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {// abort - we have a deadline we can't meetreturn null;}holder = mAdapter.createViewHolder(RecyclerView.this, type);
iOS:UITableView
iOS的UITableView相对而言逻辑就简单了很多,创建屏幕可显示最大个数+1的cell,当一个cell被移动出屏幕,自动进入到缓存池。
用网上的一张图片来描述:
机制相当简单,就不描述了
和RecyclerView相比,UITableView有两个最大的不同:
RecyclerView的复用机制是自动的,而UITableView是需要手动启动的:
看代码:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{// 0.重用标识// 被static修饰的局部变量:只会初始化一次,在整个程序运行过程中,只有一份内存static NSString *ID = @"cell";// 1.先根据cell的标识去缓存池中查找可循环利用的cellUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];// 2.如果cell为nil(缓存池找不到对应的cell)if (cell == nil) {cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];}// 3.覆盖数据cell.textLabel.text = [NSString stringWithFormat:@"testdata - %zd", indexPath.row];return cell;
}
注意dequeueReusableCellWithIdentifier:ID这个方法,从字面意思就能理解是从缓存池中获取一个cell来复用。如果你在创建cell时不是调用这个方法,而是直接新建一个cell,那么UITableView的缓存机制就没有用了。
数据清零
UITableView无视位置,cell只要回收就会清除数据,这一点其实从代码里就能一目了然,每个cell,都需要当成新建的cell来重新赋值。
iOS 和 Android:UITableView与RecycleView的重用机制比较相关推荐
- UI:UITableView 编辑、cell重用机制
tableView编辑.tableView移动.UITableViewController tableView的编辑:cell的添加.删除. 使⽤场景: 删除⼀个下载好的视频,删除联系⼈: 插⼊⼀条新 ...
- iOS开发系列--UITableView全面解析
iOS开发系列--UITableView全面解析 2014-08-23 23:20 by KenshinCui, 2202 阅读, 18 评论, 收藏, 编辑 --UIKit之UITableView ...
- (0074)iOS开发之UITableView的优化
写的很好引用 https://www.jianshu.com/p/af6b095aaaf3 前言 这篇文章对 UITableView 的优化主要从以下3个方面分析: 基础的优化准则(高度缓存, cel ...
- IOS中UITableViewCell的重用机制原理
创建UITableViewController子类的实例后,IDE生成的代码中有如下段落: - (UITableViewCell *)tableView:(UITableView *)tableVie ...
- 先进技术android,React Native实战(JavaScript开发iOS和Android应用)/计算机科学先进技术译丛...
导语 内容提要 本书作者Nader Dabit是AWS Mobile开发人员.React Native Training创始人和React Native Radio播客主持人.本书旨在帮助iOS.An ...
- iOS vs Android 系统架构
iOS是基于UNIX内核,Android是基于Linux内核,iOS和android作为两款优秀的手机操作系统,他们有共性有区别. iOS的系统架构 分为四个层次: 核心操作系统层(Core OS l ...
- iOS和android h5字体差异,关于移动hybrid开发中H5页面的字体应与系统保持一致的问题...
8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 0.问题来源 在移动hybrid开发,也就是说,部分页面会使用html+css+javascript技术来制作,例如个人 ...
- js判断是iOS还是Android
platform.js: var browser={ versions:function(){ var u = navigator.userAgent, app = navigator ...
- iOS开发-自己定义重用机制给ScrollerView加入子视图
iOS开发-自己定义重用机制给ScrollerView加入子视图 事实上这个问题我非常早就想过,仅仅是没有通过去写程序实现,昨天有人提起,我就巧了一下 不知道大家打印郭tableview:cellfo ...
最新文章
- 用jstat摸清JVM线上系统情况
- Linux参数顺序,【每日一linux命令3】参数(或称选项)顺序
- CentOS 6.3下Samba服务器的安装与配置(转)
- php如何读取多个url文件,如何从PHP中的URL获取具有相同名称的多个参数
- 为什么unity 安装完模块还是找不到sdk_Unity填坑笔记(四)——移植UWP平台
- 全国计算机一级msoffice考试内容,2015年全国计算机一级MSOffice考试大纲
- Js中Currying的应用
- python3.7运行报错_Python 3.7 环境下运行 scrapy crawl 报错 def write(self, data, async=False)?...
- java百度地图离线LBS_百度地图之离线下载功能
- 【郑州校区】BOS v2.0物流管理平台整体概述
- 使用PHP+MYSQL搭建的一款直播电商源码和大家分享一下
- 计算器计算经纬距离_经纬距离计算器下载_经纬距离计算器官方下载-太平洋下载中心...
- 微信公众号如何开通支付功能?
- 视频点播服务器项目,项目九搭建视频点播vod服务器美萍vod.doc
- (软考)系统分析师——标准化知识
- Three.js实现太阳系八大行星的自转公转
- Visual Studio系列创建工程占用空间大的解决办法
- linux系统的常用命令:在线下载数据文件解压缩
- 景联文科技:手势识别如何在自动驾驶中应用,一文告诉你答案
- 基于NLM的插值算法