ASDK对TableView进行性能调优

前言

造成iOS卡顿有很多因素,而造成这个问题大多是阻塞主线程导致用户的交互反馈出现可以感知的延迟。原因主要有一下三种情形:

1.UI 渲染需要时间较长,无法按时提交结果;
2.一些需要密集计算的处理放在了主线程中执行,导致主线程被阻塞,无法渲染 UI 界面;
3.网络请求由于网络状态的问题响应较慢,UI 层由于没有模型返回无法渲染。

上面的这些问题都会影响应用的性能,最常见的表现就是 UITableView 在滑动时没有达到 60 FPS,用户能感受到明显的卡顿。

什么是FPS

FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的动作就会愈流畅。通常,要避免动作不流畅的最低是30。来自百度文库

屏幕是如何渲染的?

iPhone图像显示工作原理


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 VS Frame

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使用ASCellNodeASCellNode通过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

2017-09-28 14_15_16.gif

可以看到还是比较流畅的,及时暴力滑动,也可以达到60FPS或者接近60FPS。
下面是通过使用Masonry做的测试:

2017-09-28 14_18_31.gif

可以发现在快速滚动的时候有明显卡顿,FPS低至40以下,存在掉帧现象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容