京东到家iOS端:UI性能提升技术实践

栏目: IOS · 发布时间: 4年前

内容简介:作者简介:京东到家-前台研发部;马兰、韵玲,负责京东到家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压力。

京东到家iOS端:UI性能提升技术实践

话说回来,如何让在列表高速滚动时也维持较高的帧率呢?从以往的经验来看,往往影响我们性能的不外乎是主线程做了很多不应该的业务处理、大量的布局计算,或出现了不能被复用的离屏渲染以及多视图层的混合,接下来举个到家商品列表的实际例子,看看到家App商品组件一行一列样式的视觉样式定义(如下图)

京东到家iOS端:UI性能提升技术实践

实际在App中使用的时候,会有很多图层,并且每一个商品的信息都不完全相同,如促销标、广告语、会员价、商品遮照说明等,高速滚动时复用命中率比较低,尽管我们已经提前在其他线程中把商品的model变胖,提前计算布局信息,但仍然会面临大量的视图层次,与不规则的圆角(7、8)、遮罩(2)甚至产生离屏渲染。

京东到家iOS端:UI性能提升技术实践

针对这种在列表内元素布局复杂的视图,到家 App 作出了一些实践,将静态的元素异步合成一张 BitMap,最后替换 layer 的 content ,并缓存起来,这样大大可以解决之前提到的问题。

方案

首先将视图重新抽象设计成 Node 对应 UIKit 中的 UIView,可以在任意线程操作,并且可以做的更轻一些。异步渲染组件提供最基本的 BaseNode、ImageNode、LabelNode等常用原子 UI 组件,对于自定义的组件可以继承 BaseNode,自定义绘制样式。

京东到家iOS端:UI性能提升技术实践

同时 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)进行缓存几个职能,总体工作流程如下图所示:

京东到家iOS端:UI性能提升技术实践

【取与舍】在UI开发中有部分的元素是可以交互的如按钮,如商品组件中的加、减车按钮,这种交换的组件如果也合成静态图片,那么有些得不偿失,反而增加了复杂度,如果异步合成那么就要处理响应区域、交互动效等问题,所以针对这种情况, - (void)asyncDrawDidFinished; 等生命周期回调函数,由业务自行添加,这样就将静态元素都合并成图片,动态资源仍然能保持原有的交换与动效。是不是有点擦边 flutter 中的 StatefulWidget 与 StatelessWidget呢?

实践

接下来看看在京东到家App中的实现效果对比吧?之前在商品 Cell 上的很多的视图层次,现在只剩下一个背景层和按钮层,对比如下图所示:

京东到家iOS端:UI性能提升技术实践

京东到家iOS端:UI性能提升技术实践

可以看出使用后视图层次变成了一张,里面的子视图的处理都在异步线程进行处理,减少了主线程的占用和GPU的负载压力。我们接下来再看一下滚动过程中的性能对比,如下图所示:

京东到家iOS端:UI性能提升技术实践

从主线程的峰值和均值角度来看异步渲染是占有相当大的优势的,从帧率波动上看,也有非常不错的提升,在高端机型全程 60FPS ,找一个iphone 5s 上得到如下对比数据:

京东到家iOS端:UI性能提升技术实践

总结

通过实践得出,从传统UI开发到异步合成并渲染的方案确实有很大的提升,随着CPU核心越来越多、越来越强劲,充分的利用其他核心帮助分摊主线程和GPU的压力也是一种优化思路,值得反思的是,在 App 的 UI设计上中能够用简洁的设计完成展现,就不要用复杂的特效或显示效果,带来的计算与渲染成本非常高,所以简单明了的设计是一个非常良好的开始。

京东到家iOS端:UI性能提升技术实践


以上所述就是小编给大家介绍的《京东到家iOS端:UI性能提升技术实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

首席增长官

首席增长官

张溪梦 / 机械工业出版社 / 2017-11-1 / 69.9

增长是企业永恒的主题,是商业的本质。 人口红利和流量红利的窗口期正在关闭,曾经“流量为王”所带来的成功经验正在失效,所造成的思维逻辑和方法论亟待更新。在互联网下半场,企业要如何保持增长?传统企业是否能跟上数字化转型的脚步,找到新兴业务的增长模式?为什么可口可乐公司用首席增长官取代了首席营销官职位? 数据驱动增长正在成为企业发展的必需理念,首席增长官、增长团队和增长黑客将是未来商业的趋势......一起来看看 《首席增长官》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具