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

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

内容简介:作者简介:京东到家-前台研发部;马兰、韵玲,负责京东到家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性能提升技术实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Web Security Testing Cookbook

Web Security Testing Cookbook

Paco Hope、Ben Walther / O'Reilly Media / 2008-10-24 / USD 39.99

Among the tests you perform on web applications, security testing is perhaps the most important, yet it's often the most neglected. The recipes in the Web Security Testing Cookbook demonstrate how dev......一起来看看 《Web Security Testing Cookbook》 这本书的介绍吧!

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

在线压缩/解压 HTML 代码

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

HTML 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码