内容简介:作者简介:京东到家-前台研发部;马兰、韵玲,负责京东到家App核心模块的开发、维护等,例如,门店、购物车,等等;冰洋,负责App架构设计、优化、新技术预研落地等。背景随着到家App的发展,页面承载信息越来越多,所以单位面积内的视觉元素也越来越多,随着功能迭代与视觉改版就会有产生出很多复杂的组件,并且将这些元素紧密的排列挤在一个有限的空间里。如果元素
作者简介:京东到家-前台研发部;马兰、韵玲,负责京东到家App核心模块的开发、维护等,例如,门店、购物车,等等;冰洋,负责App架构设计、优化、新技术预研落地等。
背景
随着到家App的发展,页面承载信息越来越多,所以单位面积内的视觉元素也越来越多,随着功能迭代与视觉改版就会有产生出很多复杂的组件,并且将这些元素紧密的排列挤在一个有限的空间里。如果元素 这么多 并且这些元素又拥有的阴影、圆角、渐变这些视觉特效,那么如何能保持住页面流畅视觉体验,成为了我们值得挑战的事情。
分析
一般来说,UI性能不佳,往往最容易体现在大列表滚动的时候,给用户的第一感觉来说就是滚动起来“有点卡”,话说有点卡也就是我们常说的掉帧了,什么是帧率呢,经常玩游戏的同学,会关注在游戏运行时的帧率,也就是FPS(frames per second 即:每秒显示帧数),一般来说想要达到流畅则需要 60FPS,也就是每秒会有 60 副画面在你的眼前刷新,由于刷新的非常快,所以就会感觉到很流畅,当刷新率低于 50FPS,遇到快速的变化时就会给人感觉有些延迟,不够流畅,当低于 30FPS,那么就会出现明显的卡顿现象。
接下来我们看看什么决定帧率呢?在iOS 设备使用的是双缓存机制+垂直同步机制,一般来说主线程(UI线程)在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等,随后 CPU 会将计算好的内容提交到 GPU 中,由 GPU 来完成变换、合成、渲染,随后 GPU 将渲染结果提交到帧缓存区,当 VSync (垂直同步信号:告诉电子枪该显示下一帧了, 即该回左上角起始点了画下一帧了) 信号到来的时候显示到屏幕上。由于垂直同步机制,如果在 1/60(60FPS)秒内,CPU 或者 GPU 没有完成这一帧的处理,那么这一帧就不会被显示在屏幕上,等待下一次 VSync 信号,显示屏会保持之前的显示的内容,不会更新到最新显示内容,这样就造成了丢帧,帧丢多了,就是我们说的卡顿,不论是 CPU 还是 GPU 处理超时,都会造成掉帧(如下图所示),所以在实际开发中需要合理的使用CPU(特别是主线程资源)与分摊GPU压力。
话说回来,如何让在列表高速滚动时也维持较高的帧率呢?从以往的经验来看,往往影响我们性能的不外乎是主线程做了很多不应该的业务处理、大量的布局计算,或出现了不能被复用的离屏渲染以及多视图层的混合,接下来举个到家商品列表的实际例子,看看到家App商品组件一行一列样式的视觉样式定义(如下图)
实际在App中使用的时候,会有很多图层,并且每一个商品的信息都不完全相同,如促销标、广告语、会员价、商品遮照说明等,高速滚动时复用命中率比较低,尽管我们已经提前在其他线程中把商品的model变胖,提前计算布局信息,但仍然会面临大量的视图层次,与不规则的圆角(7、8)、遮罩(2)甚至产生离屏渲染。
针对这种在列表内元素布局复杂的视图,到家 App 作出了一些实践,将静态的元素异步合成一张 BitMap,最后替换 layer 的 content ,并缓存起来,这样大大可以解决之前提到的问题。
方案
首先将视图重新抽象设计成 Node 对应 UIKit 中的 UIView,可以在任意线程操作,并且可以做的更轻一些。异步渲染组件提供最基本的 BaseNode、ImageNode、LabelNode等常用原子 UI 组件,对于自定义的组件可以继承 BaseNode,自定义绘制样式。
同时 BaseNode 封装的方式更近于UIView,支持很多跟 UIView一样的属性与方法,甚至与根据业务的特性支持渐变色、不规则圆角等一些特殊样式,这样其他UI组件的开发人员可以快速继承 BaseNode 完成子 Node 封装与开发。
// // DJAsyncBaseNode.h // DJAsyncRenderModule // // Created by Cooriyou on 2018/2/18. // #import <Foundation/Foundation.h> #import "DJAsyncAssistant.h" @class DJAsyncContainer; @interface DJAsyncBaseNode : NSObject /** The frame rectangle, which describes the node’s location and size in its supernode’s coordinate system. */ @property(nonatomic,assign) CGRect frame; /** The default value is 0. You can set the value of this tag and use that value to identify the node later. */ @property(nonatomic,assign) NSInteger tag; /** A Boolean value that determines whether the node is hidden. */ @property(atomic,assign) BOOL hidden; /** A Boolean value that determines whether subnodes are confined to the bounds of the node. */ @property(atomic,assign) BOOL clipsToBounds; /** The node’s background color. */ @property(nullable, atomic, copy) UIColor *backgroundColor; /** The node’s background start color. */ @property (atomic, nullable, copy) UIColor *backgroundStartColor; /** The node’s background end color. */ @property (atomic, nullable, copy) UIColor *backgroundEndColor; /** The node’s border color. */ @property (atomic, nullable, copy) UIColor *borderColor; /** The node’s border width. */ @property (atomic, assign) CGFloat borderWidth; /** The radius to use when drawing rounded corners for the node’s background. */ @property (nonatomic, assign) CGFloat cornerRadius; /** The radius to use when drawing rounded custom corners(TLBR) for the node’s background. */ @property (nonatomic, assign) DJAsyncBaseNodeRadius radius; /** The clipPath for the node’s background.(read only) */ @property (atomic, readonly, strong) UIBezierPath * _Nullable clipPath; /** The node's supernode, or nil if it has none. */ @property(nullable, nonatomic,readonly,weak) DJAsyncBaseNode *superNode; /** The node’s immediate subnodes. */ @property(nonatomic,readonly,strong) NSMutableArray<__kindof DJAsyncBaseNode *> *subNodes; /** The node's rootnode , or nil if it has none. */ @property(nonatomic,weak) DJAsyncContainer * _Nullable containerRef; /** Unlinks the node from its supernode. */ - (void)removeFromSuperNode; /** Adds a node to the end of the node’s list of subnodes. @param node The node to be added. */ - (void)addSubNode:(DJAsyncBaseNode *)node; /** Draws the node’s image within the passed-in rectangle and context. @param rect frame @param context current context */ - (void)asyncDrawRect:(CGRect)rect inContext:(CGContextRef _Nonnull ) context; /** The node's absoluteRect @return node's absoluteRect */ - (CGRect)absoluteRect; @end
完成了原子组件的定义,接下来就是解决 Node 的异步合成绘制与展现工作,首先我们会有一个容器View,当需要绘制的时机来到时,如 - (void)displayLayer:(CALayer *)layer
被调用,则就将 Nodes 就交给 AsyncDrawEngine进行绘制,这个异步绘制主要包含线程池中的绘制线程的生命周期管理、绘制任务分配、绘制执行并将绘制结果(Image)进行缓存几个职能,总体工作流程如下图所示:
【取与舍】在UI开发中有部分的元素是可以交互的如按钮,如商品组件中的加、减车按钮,这种交换的组件如果也合成静态图片,那么有些得不偿失,反而增加了复杂度,如果异步合成那么就要处理响应区域、交互动效等问题,所以针对这种情况, - (void)asyncDrawDidFinished;
等生命周期回调函数,由业务自行添加,这样就将静态元素都合并成图片,动态资源仍然能保持原有的交换与动效。是不是有点擦边 flutter 中的 StatefulWidget 与 StatelessWidget呢?
实践
接下来看看在京东到家App中的实现效果对比吧?之前在商品 Cell 上的很多的视图层次,现在只剩下一个背景层和按钮层,对比如下图所示:
可以看出使用后视图层次变成了一张,里面的子视图的处理都在异步线程进行处理,减少了主线程的占用和GPU的负载压力。我们接下来再看一下滚动过程中的性能对比,如下图所示:
从主线程的峰值和均值角度来看异步渲染是占有相当大的优势的,从帧率波动上看,也有非常不错的提升,在高端机型全程 60FPS ,找一个iphone 5s 上得到如下对比数据:
总结
通过实践得出,从传统UI开发到异步合成并渲染的方案确实有很大的提升,随着CPU核心越来越多、越来越强劲,充分的利用其他核心帮助分摊主线程和GPU的压力也是一种优化思路,值得反思的是,在 App 的 UI设计上中能够用简洁的设计完成展现,就不要用复杂的特效或显示效果,带来的计算与渲染成本非常高,所以简单明了的设计是一个非常良好的开始。
以上所述就是小编给大家介绍的《京东到家iOS端:UI性能提升技术实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Flutter到家助手实践
- 京东到家订单派发的技术实战
- 京东到家订单中心 Elasticsearch 演进历程
- 58到家mysql数据库军规及解读分享
- 从0到1达达-京东到家支付系统设计理念介绍
- 【包邮到家】免费送15本畅销书籍!| 数据分析、Python等!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python核心编程(第3版)
[美] Wesley Chun / 孙波翔、李斌、李晗 / 人民邮电出版社 / 2016-5 / CNY 99.00
《Python核心编程(第3版)》是经典畅销图书《Python核心编程(第二版)》的全新升级版本,总共分为3部分。第1部分为讲解了Python的一些通用应用,包括正则表达式、网络编程、Internet客户端编程、多线程编程、GUI编程、数据库编程、Microsoft Office编程、扩展Python等内容。第2部分讲解了与Web开发相关的主题,包括Web客户端和服务器、CGI和WSGI相关的We......一起来看看 《Python核心编程(第3版)》 这本书的介绍吧!