iOS 高性能异构滚动视图构建方案 —— LazyScrollView

简介:

##LazyScroll是什么

LazyScrollView 继承自ScrollView,目标是解决异构(与TableView的同构对比)滚动视图的复用回收问题。它可以支持跨View层的复用,用易用方式来生成一个高性能的滚动视图。此方案最先在天猫iOS客户端的首页落地。

为什么要用LazyScrollView

猫客首页之前首页的View比较少,不需要复用和回收也有很优秀的性能,但是之后首页的View数量逐渐膨胀,没有一套复用回收机制的ScrollView已经影响到性能了,迫切需要处理对ScrollView中View的复用和回收。

使用TableView只能用来解决同类Cell的展示,而在猫客首页这个ScrollView里面,View的种类太多了。不适合我们的场景。

而UICollectionView本身的布局和复用回收机制不够灵活,用起来也较为繁琐。并且本来猫客的首页就有一套相对成熟的卡片布局方案。所以诞生了LazyScrollView去解决这个问题。

LazyScroll如何用

LazyScrollView的使用和TableView很像,不过多了一个需要实现的方法:返回对应index的View 相对LazyScrollView的绝对坐标。

实现LazyScrollViewDatasource

类似TableView的用法,我们需要使用方实现LazyScrollViewDatasource这个Delegate

@protocol TMMuiLazyScrollViewDataSource <NSObject>
@required
//ScrollView一共展示多少个item
- (NSUInteger)numberOfItemInScrollView:(TMMuiLazyScrollView *)scrollView;
//要求根据index直接返回RectModel
- (TMMuiRectModel *)scrollView:(TMMuiLazyScrollView *)scrollView rectModelAtIndex:(NSUInteger)index;
	//返回下标所对应的view
- (UIView *)scrollView:(TMMuiLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID;
@end

LazyScrollView的核心是在初始状态就得知所有View应该显示的位置。这个Protocol可以让LazyScrollView获取到这些信息。

第一个方法很简单,获取LazyScrollView中item的个数。

第二个方法需要按照Index返回TMMuiRectModel ,它会携带对应index的View 相对LazyScrollView的绝对坐标。TMMuiRectModel是这么个东西:

@interface TMMuiRectModel:NSObject
//转换后的绝对值rect
@property (nonatomic,assign) CGRect absRect;
//业务下标
@property (nonatomic,copy) NSString *muiID;

@end

absRect是LazyScroll中的View相对LazyScrollView的绝对坐标,muiID是这个View在LazyScrollView中唯一的标识符,可赋值也可不赋值,不赋值的话LazyScroll会处理成转换为字符串的下标。如果这个标识符在Protocol的第三个方法中会用到。

第三个方法,返回View。首先,我们在UIView之外加了一个Category:

@interface UIView(TMMui)

//索引过的标识,在LazyScrollView范围内唯一
@property (nonatomic, copy) NSString  *muiID;
//重用的ID
@property (nonatomic, copy) NSString *reuseIdentifier;

这个category可以让View携带muiID和reuseIdentifier,对于返回的View来说,只需要在乎对View的reuseIdentifier赋值,muiID的赋值会在lazyScrollView中处理掉。reuseIdentifier相同的View会被复用,如果这个View的reuseIdentifier是nil或者空字符串,则不会被复用。

调用核心API

- (void)reloadData;

重新走一遍DataSource的这些方法,等同于TableView中的reloadData

- (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier

根据identifier获取可以复用的View。和TableView的dequeueReusableCellWithIdentifier:(NSString *)identifier方法意义相同。通常是在LazyScrollViewDatasource第三个方法,返回View的时候使用。先尝试获取复用池中的View,如果没有再去新建。

LazyScrollView的内部实现

这是一个Demo, 被复用的View,标记的backgroundColor会和之前生成的时候有所不同。

STEP 1 根据DataSource获取所有的TMMuiRectModel

根据DataSource的Delegate,拿到所有的View应该被显示的位置。这一步,核心是拿到的位置是确定的。

根据Demo,我们观察从 0/1 - 2/3 之间这些View

这个时候LazyScrollView拿到的Rect如下:

Index 标号(MUIID) Rect
0 0/0 origin = (x = 25, y = 15), size = (width = 156, height = 150
1 0/1 origin = (x = 194, y = 15), size = (width = 156, height = 150)
2 0/2 origin = (x = 25, y = 180), size = (width = 156, height = 150)
3 0/3 origin = (x = 194, y = 180), size = (width = 156, height = 150
4 1/0 origin = (x = 5, y = 360), size = (width = 177.5, height = 150)
5 1/1 origin = (x = 192.5, y = 426), size = (width = 84, height = 84)
6 1/2 origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56)
7 1/3 origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84)
8 2/0 origin = (x = 25, y = 530), size = (width = 325, height = 150)
9 2/1 origin = (x = 25, y = 695), size = (width = 325, height = 150)
10 2/2 origin = (x = 25, y = 860), size = (width = 325, height = 150)

STEP 2 排序

拿到了这些位置之后,接下来做的事情就是排序。排序生成的索引会有两个:根据顶边(y)升序排序的索引和根据底边(y+height)降序排序的索引。

根据顶边(y)升序排序的索引

RANK 标号(MUIID) Rect
0 0/0 origin = (x = 25, y = 15), size = (width = 156, height = 150
1 0/1 origin = (x = 194, y = 15), size = (width = 156, height = 150)
2 0/2 origin = (x = 25, y = 180), size = (width = 156, height = 150)
3 0/3 origin = (x = 194, y = 180), size = (width = 156, height = 150
4 1/0 origin = (x = 5, y = 360), size = (width = 177.5, height = 150)
5 1/2 origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56)
6 1/1 origin = (x = 192.5, y = 426), size = (width = 84, height = 84)
7 1/3 origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84)
8 2/0 origin = (x = 25, y = 530), size = (width = 325, height = 150)
9 2/1 origin = (x = 25, y = 695), size = (width = 325, height = 150)
10 2/2 origin = (x = 25, y = 860), size = (width = 325, height = 150)

根据底边(y+height)降序排序的索引

RANK 标号(MUIID) Rect
0 2/2 origin = (x = 25, y = 860), size = (width = 325, height = 150)
1 2/1 origin = (x = 25, y = 695), size = (width = 325, height = 150)
2 2/0 origin = (x = 25, y = 530), size = (width = 325, height = 150)
3 1/0 origin = (x = 5, y = 360), size = (width = 177.5, height = 150)
4 1/2 origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56)
5 1/3 origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84)
6 1/1 origin = (x = 192.5, y = 426), size = (width = 84, height = 84)
7 0/2 origin = (x = 25, y = 180), size = (width = 156, height = 150)
8 0/3 origin = (x = 194, y = 180), size = (width = 156, height = 150
9 0/0 origin = (x = 25, y = 15), size = (width = 156, height = 150
10 0/1 origin = (x = 194, y = 15), size = (width = 156, height = 150)

STEP 3 查找

前两步是在执行完reload,在视图还没有生成的时候就开始做了,而接下来的步骤在要生成视图(初始化或滚动的时候)才会去做。

我们设定了Buffer为上下各20,滚动超过20个像素后才会指定查找视图并显示的动作。

接下来就是找哪些View应该被显示了。举个例子,如下图,红圈是应该显示的区域。

现在已知的是红圈顶边y是242,底边y是949,加上缓冲区Buffer,应该是找222 - 969 之间的View。我们要做的是,找到底边y小于969的Model顶边y大于222的Model,取交集,就是我们要显示的View

采用的方法为二分查找,在根据顶边升序排序的索引中找949,找到的index为0(MUIID为2/2),我们使用一个Set,把根据顶边排序中index >= 0 的元素先放在这里。获取的Set中包含的muiID为 0/0,0/1,0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2

根据底边排序的索引中找222,找到的index为2,我们把index >= 2的元素放在另一个Set,获取的Set中包含的muiID为0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2

两个Set取交集,得到的就是我们的ResultSet,这里面都是我们要显示View的Model,它们的muiID是0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2

STEP 4 回收、复用、生成

我们知道了应该显示哪些View,但是我们之后做的第一步是把不需要显示的View加入到复用池中。

LazyScroll可以取到当前显示了的View,拿当前显示的View的muiID和将要显示view的Model的muiID做对比,可以知道当前显示的View哪些应该被回收。

LazyScrollView中有一个Dictionary,key是reuseIdentifier,Value是对应reuseIdentifier被回收的View,当LazyScrollView得知这个View不该再出现了,会把View放在这里,并且把这个View hidden掉。

接下来,LazyScrollView会去调用datasource的- (UIView *)scrollView:(TMMuiLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID; 复用还是不复用,是由datasource决定的。如果要复用,需要datasource方法内调用- (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier获取复用的View,这个方法取出来的View就是在上一段所说的Dictionary中拿的。

这样,我们就完成了一次完整的循环 : 找到所有View将要显示的位置 – 排序 – 查找应该显示的View – 回收 – 创建/复用。

##最后

LazyScroll的复用和回收能力是比较强大的,在猫客首页使用之后,因为View数量而导致内存过多的问题得到了解决。

在这套复用和回收机制的加持之下,我们将LazyScrollView继续延伸,构造出一套完整的布局解决方案Tangram,它将Native中View的布局方式变得更动态化,敬请期待。

目录
相关文章
|
3月前
|
存储 数据建模 数据库
IOS开发数据存储:什么是 UserDefaults?有哪些替代方案?
IOS开发数据存储:什么是 UserDefaults?有哪些替代方案?
66 0
|
17天前
|
Swift iOS开发 开发者
探索iOS开发:使用SwiftUI构建现代应用程序
【8月更文挑战第3天】在本文中,我们将一起潜入SwiftUI的世界,这是一个创新的框架,它让iOS应用的开发变得更加简单快捷。我们将通过一个实际的项目示例来展示如何利用SwiftUI的强大功能,快速搭建起一个既美观又实用的应用界面。无论你是刚入门的新手还是寻求提升的开发者,这篇文章都将为你提供宝贵的知识和技能。
38 10
|
5天前
|
开发工具 Android开发 iOS开发
探索安卓与iOS开发的差异:构建未来应用的关键考量
在数字时代,选择正确的开发平台是成功的一半。本文深入探讨了安卓与iOS两大移动操作系统的开发差异,并分析了各自对创新、用户体验和市场需求的响应。通过比较两者的设计哲学、开发工具、市场覆盖和用户参与度,我们揭示了每个平台的独特优势和潜在挑战,旨在为开发者提供决策时的洞见,帮助他们在竞争激烈的应用市场中做出明智的选择。
|
1月前
|
安全 Java Android开发
探索安卓与iOS开发的差异:构建未来应用的关键考量
【7月更文挑战第18天】在移动应用开发的广阔天地中,安卓和iOS两大平台各领风骚。本文将深入探讨这两个平台在开发过程中的主要差异,包括编程语言、用户界面设计、性能优化、安全性以及市场策略等方面。通过比较分析,旨在为开发者提供决策支持,帮助他们选择最适合自己项目需求的平台,同时考虑到用户体验和市场需求的变化,为未来的应用开发指明方向。
|
19天前
|
机器学习/深度学习 JSON 开发工具
探索iOS开发:构建你的第一个天气应用深度学习在图像识别中的应用与挑战
【7月更文挑战第31天】在数字时代的浪潮中,移动应用成为了连接世界与个人的重要桥梁。本文将带你走进iOS开发的世界,通过一个实际的项目——构建一个简单的天气应用,来展示如何利用Swift编程语言和苹果的开发工具进行iOS应用开发。我们将从设置开发环境开始,逐步介绍界面设计、数据获取、以及功能实现等关键步骤。无论你是编程新手还是希望扩展技能的开发者,这篇文章都将为你提供一个清晰的指南,帮助你理解并实施一个完整的iOS应用开发过程。让我们开始吧,一起创造属于你的第一款iOS应用! 【7月更文挑战第31天】深度学习作为人工智能领域的重要分支,已经在多个领域取得了显著的成果。特别是在图像识别方面,深
29 0
|
1月前
|
iOS开发 开发者 UED
探索iOS开发中的SwiftUI框架:构建现代应用程序的桥梁
本文深入探讨了SwiftUI框架在iOS开发中的应用,揭示了其对提升开发效率和改善用户体验的重要性。通过分析SwiftUI的设计哲学、核心组件以及与旧有开发模式的比较,文章旨在为读者提供关于如何利用SwiftUI构建现代化应用程序的实用指南。
|
3月前
|
安全 Swift iOS开发
【Swift 开发专栏】Swift 与 UIKit:构建 iOS 应用界面
【4月更文挑战第30天】本文探讨了Swift和UIKit在构建iOS应用界面的关键技术和实践方法。Swift的简洁语法、类型安全和高效编程模型,加上与UIKit的紧密集成,使开发者能便捷地创建用户界面。UIKit提供视图、控制器、布局、动画和事件处理等功能,支持灵活的界面设计。实践中,遵循设计原则,合理组织视图层次,运用布局和动画,以及实现响应式设计,能提升界面质量和用户体验。文章通过登录、列表和详情界面的实际案例展示了Swift与UIKit的结合应用。
179 1
|
3月前
|
安全 Android开发 iOS开发
构建未来:安卓与iOS的无缝集成技术探索
【5月更文挑战第20天】随着智能设备的普及和技术的不断进步,安卓和iOS两大操作系统之间的界限正在逐渐模糊。本文将深入探讨如何通过最新的API、框架和工具实现安卓与iOS应用的无缝集成,以及这一趋势对开发者和用户的潜在影响。我们将从技术可行性、安全性挑战、用户体验优化等角度出发,分析当前的发展状况,并展望未来可能的技术融合路径。
|
3月前
|
存储 编解码 JSON
利用SwiftUI构建高效iOS天气应用
【4月更文挑战第21天】 在本文中,我们将深入探讨如何运用SwiftUI框架打造一个响应迅速且用户友好的iOS天气应用程序。我们将重点放在利用SwiftUI的声明式语法简化界面开发,并通过结合Core Location和Networking APIs实现实时天气数据的获取与展示。文章将详细阐述整个开发过程,包括API集成、数据模型设计、用户界面布局以及动态适配不同屏幕尺寸的策略。
|
3月前
|
iOS开发 开发者 UED
利用SwiftUI构建动态列表:iOS开发的新范式
【4月更文挑战第22天】在本文中,我们将深入探讨如何使用SwiftUI来创建动态列表。SwiftUI是苹果最新推出的用户界面工具集,它允许开发者以声明式的方式描述用户界面,从而简化了代码的复杂性。我们将通过具体的代码实例,展示如何利用SwiftUI的List和ForEach视图来创建动态列表,并讨论其在实际开发中的应用。
52 2