前言
造成iOS卡顿有很多因素,而造成这个问题大多是阻塞主线程导致用户的交互反馈出现可以感知的延迟。原因主要有一下三种情形:
1.UI 渲染需要时间较长,无法按时提交结果;
2.一些需要密集计算的处理放在了主线程中执行,导致主线程被阻塞,无法渲染 UI 界面;
3.网络请求由于网络状态的问题响应较慢,UI 层由于没有模型返回无法渲染。
上面的这些问题都会影响应用的性能,最常见的表现就是 UITableView 在滑动时没有达到 60 FPS,用户能感受到明显的卡顿。
什么是FPS
FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的动作就会愈流畅。通常,要避免动作不流畅的最低是30。来自百度文库
屏幕是如何渲染的?
iPhone图像显示工作原理
iOS 的显示系统是由 VSync脉冲信号驱动的,VSync脉冲信号由硬件时钟生成,大约每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)
iPhone采用的双缓冲,安卓是三缓冲,各有优缺点,但都是各自的最优解决方案
通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。
1.CPU 计算好显示内容提交到 GPU,
2.GPU 渲染完成后将渲染结果放入第一缓冲区,
3.然后硬件 Vsync到来,第二缓冲会copy第一缓冲待显示内容,因为交换内存地址,可认为是瞬间完成
4.屏幕硬件经过数模转换传递而显示出来
卡顿为什么会产生呢?
所以我们做性能优化,要从减轻cpu和gpu负担的角度去思考
性能优化之AutoLayout VS Frame(减轻CPU负担)
Autolayout 是自iOS 6之后 苹果引入一种“自动布局”技术,
苹果官方也大力推荐开发者使用此来进行UI布局
AutoLayout其实最终会转化为对 UIView.frame/bounds/center 等属性的调整。
随着视图数量的增加, Autolayout 带来的 CPU 消耗会呈指数级上升
所以建议大家,在需要优先考虑性能的界面,使用frame。
优化性能之GUP
大多数的 CALayer 的属性都是由 GPU 来绘制的,比如图片的圆角、变换、应用纹理;但是过多的几何结构、重绘、离屏绘制(Offscrren)以及过大的图片都会导致 GPU 的性能明显降低。
Texture
Texture
字面上是纹理的意思,以前叫做AsyncDisplayKit
,是由 Facebook 开源的一个 iOS 框架,能够帮助最复杂的 UI 界面保持流畅和快速响应。下文简称为ASDK。
ASDK的作者是Scott Goodson,他本来是在苹果工作,开发了很多苹果内置的应用。后来到了Facebook,开发了ASDK这款产品。现在他就职于youtube。
ASDK 从开发到开源大约经历了一年多的时间,它其实并不是一个简单的框架,更像是对 UIKit 的重新实现,把整个 UIKit
以及 CALayer
层封装成一个一个 Node
,将昂贵的渲染、图片解码、布局以及其它 UI 操作移出主线程,这样主线程就可以对用户的操作及时做出反应。
在 ASDK 中最基本的单位就是 ASDisplayNode
,每一个 node 都是对UIView
以及CALayer
的抽象。但是与UIView
不同的是,ASDisplayNode
是线程安全的,它可以在后台线程中完成初始化以及配置工作。
如果按照 60 FPS 的刷新频率来计算,每一帧的渲染时间只有 16ms,在 16ms 的时间内要完成对 UIView 的创建、布局、绘制以及渲染,CPU 和 GPU 面临着巨大的压力。
从 A5 处理器之后,多核的设备成为了主流,原有的将所有操作放入主线程的实践已经不能适应复杂的 UI 界面,所以 ASDK 将耗时的 CPU 操作以及 GPU 渲染纹理(Texture)的过程全部放入后台进程,使主线程能够快速响应用户操作。
ASTableNode
ASTableNode
类似于 UIKit
中的 UITableView
,但是又有着很大不同。ASTableNode
的cell使用ASCellNode
,ASCellNode
通过FlexBox进行布局,性能基本和Frame相持平。
ASTableNode
并没有像 UITableView 一样提供一个-tableView:heightForRowAtIndexPath:协议方法来决定每个 Cell 的高度,而是由 ASCellNode 本身决定。这样带来的另外一个好处是,动态高度的实现可谓是易如反掌。
实现代码:
ASTableNode
初始化和 UITableView
是一样的:
ASImageNode *imageNode = [[ASImageNode alloc] init];
imageNode.image = [UIImage imageNamed:@"cat.jpg"];
imageNode.frame = CGRectMake(0, 100, 100, 100);
[imageNode addTarget:self action:@selector(imageAction) forControlEvents:ASControlNodeEventTouchUpInside];
[self.view addSubnode:imageNode];
ASNetworkImageNode *node = [[ASNetworkImageNode alloc] init];
node.defaultImage = [UIImage imageNamed:@"cat.jpg"];
node.URL = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1506530505693&di=68df3b2b8d7dad413af8c92a065712dc&imgtype=0&src=http%3A%2F%2Fpic.k73.com%2Fup%2Fsoft%2F2017%2F0907%2F153257_88565218.png"];
node.frame = CGRectMake(0, 300, 100, 100);
[node addTarget:self action:@selector(imageAction2) forControlEvents:ASControlNodeEventTouchUpInside];
[self.view addSubnode:node];
ASTableNode
的dataSource协议方法和UITableView
一一对应:
/**
返回rows数量
*/
- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section {
// 1
return 500;
}
/**
相当于UITableView的cellForIndexPath
ASCellNode 是 UITableViewCell 的对应封装。本来我们可以直接返回 ASCellNode(ASDK 有一个返回这个类型的数据源方法),但是官方推荐我们使用返回块的版本。因此这里定义了一个 ASCellNodeBlock 对象,这是一个特殊的块,会返回一个 ASCellNode,不需要提供任何参数。在这个块中,我们使用了一个自定义的 ASCellNode,名为 StatusNode(后面我们会实现它),并调用它的初始化方法把模型传递进去,然后在块的最后返回这个 ASCellNode。
*/
- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
{
ASCellNode *(^ASCellNodeBlock)() = ^ASCellNode *() {
StatusNode *cellNode = [[StatusNode alloc] init];
[cellNode setData];
return cellNode;
};
return ASCellNodeBlock;
}
- (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode{
// 4
return 1;
}
ASTableNode
的delegate协议方法和UITableView
有着很大的不同:
/**
这个方法用于告诉 ASTableNode,用户的一次下拉动作是否需要触发异步抓取,这里我们返回了 YES,也就是不管什么情况都进行异步抓取。我们这样做的原因,是现在的后台服务从来不告诉前端什么时候数据才会”完”,反正有数据的话服务器会返回数据,没数据的话则返回错误(比如“ 404 没有数据” 之类)或者返回空结果集。所以我们根本无法事先知道数据什么时候数据已经加载完。所以不管数据有没有完,我们都当做没有完来进行抓取,并通过服务器返回的结果来判断。这样这个方法就没有必要进行任何计算了,直接返回 YES。
*/
- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode {
return YES;
}
/**
这个方法用于进行一次抓取。loadPageWithContext: 方法是我们自定义的,它会加载一页数据,同时页数会累加,这样每次都会加载“下一页”,除非服务器没有数据返回。context 参数是必须的,用于抓取完后通知 ASTableNode 抓取完成。
*/
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context
{
[context beginBatchFetching];
// 进行数据拉取
// [self loadPageWithContext:context];
}
- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[tableNode deselectRowAtIndexPath:indexPath animated:YES];
// 你自己的代码
// ......
}
ASCellNode
ASCellNode
类似于 UIKit
中的 UITableViewCell
,使用方式和UITableViewCell
类似,这里主要讲ASCellNode
对控件进行FlexBox的布局方式。
/**
布局的时候需要对下面四种方法实现其一:
*提供 layoutSpecBlock
*覆写 - layoutSpecThatFits: 方法
*覆写 - calculateSizeThatFits: 方法
*覆写 - calculateLayoutThatFits: 方法
layoutSpecThatFits类似于layoutSpecBlock,其实和layoutSpecBlock没什么不同
*/
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
/**
ASStackLayoutDirectionHorizontal为水平方式
justify-content属性定义了项目在主轴上的对齐方式。
align-items属性定义项目在交叉轴上如何对齐。
*/
ASStackLayoutSpec *avatarStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:5
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsCenter
children:@[_avatarNode,_nameNode,_timeNode]];
ASStackLayoutSpec *likeStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:5
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsCenter
children:@[_likeNode,_viewsNode]];
/**
这里是垂直约束
将avatarStack和likeStack以及标题看做一个整体
*/
ASStackLayoutSpec *contentStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
spacing:5
justifyContent:ASStackLayoutJustifyContentSpaceBetween
alignItems:ASStackLayoutAlignItemsStretch
children:@[avatarStack,_titleNode,likeStack]];
/**
外层加上整体边框
*/
return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(15, 15, 15, 15)
child:contentStack];
}
我们分别通过使用ASDK和Masonry对同样的界面进行性能对比,Demo 地址。
通过ASDK
可以看到还是比较流畅的,及时暴力滑动,也可以达到60FPS或者接近60FPS。
下面是通过使用
Masonry
做的测试:
可以发现在快速滚动的时候有明显卡顿,FPS低至40以下,存在掉帧现象。