一、UI组件性能
应用启动后页面加载和渲染的性能与FrameNode树上的节点数量和以及每个节点上的属性相关。
1、避免在自定义组件的生命周期内执行高耗时操作
自定义组件创建完成之后,在build函数执行之前,将先执行aboutToAppear()生命周期回调函数。此时若在该函数中执行耗时操作,将阻塞UI渲染,增加UI主线程负担。
解决:
-
对于复杂计算的耗时场景,可以将计算结果进行缓存处理。
-
对于不需要等待结果的高耗时任务,可以采用多线程处理该任务,通过并发的方式避免主线程阻塞。
-
在aboutToAppear()生命周期函数内建议只做当前组件的初始化逻辑,其他业务逻辑可以按需提前或延后处理。
eg:在生命周期aboutToAppear中应该避免使用ResourceManager的getXXXSync接口入参中直接使用资源信息,推荐使用资源id作为入参,推荐用法为:resourceManager.getStringSync($r('app.string.test').id)。
aboutToAppear(): void { hiTraceMeter.startTrace('getStringSync', 1); // 反例:getStringSync接口的入参直接使用资源,未使用资源ID getContext().resourceManager.getStringSync($r('app.string.app_name')); // 正例:getStringSync接口的入参使用了资源ID getContext().resourceManager.getStringSync($r('app.string.app_name').id); hiTraceMeter.finishTrace('getStringSync', 1); }
2、按需注册组件属性
某个组件设置了大量属性且该组件在应用中被大量使用时,单个属性的设置对应用的整体性能会产生较大影响。
解决:
-
可以考虑采用AttributeModifier动态注册组件属性的方式,替换使用属性方法静态注册组件属性的方式。
两者区别:
动态注册属性:支持开发者自定义AttributeModifier接口的实现类。 当应用运行时,系统调用AttributeModifier接口的实现类中与组件样式相关的方法,在该方法内按照开发者自定义的业务逻辑动态设置组件属性。
Diff更新属性:当组件创建或者更新时,重新执行组件的样式属性对象的更新接口。属性Diff逻辑基于Map实现,其中key值是属性类型,value值是属性修改器对象。更新场景下,通过key找到对应的属性修改器对象进行Diff对比,若有更新变化再通知native侧进行属性更新。
3、优先使用@Builder方法代替自定义组件
在自定义组件的build阶段将在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点,无论是FrameNode节点树还是RenderNode节点过多都会造成加载或渲染时间延长。
解决:
-
在应用中使用自定义组件时,可以优先考虑使用@Builder函数代替自定义组件,@Builder函数不会在后端FrameNode节点树上创建一个新的树节点
二、状态管理
由于ArkUI采用了MVVM模式,其中ViewModel将数据与视图绑定在一起,更新数据的时候直接更新视图。因此在ArkUI的开发过程中,如果没有选择合适的装饰器或合理的控制状态更新范围,可能会导致以下问题:
-
状态和UI的不一致,如同一状态的界面元素展示的UI不同,或UI界面展示的不是最新的状态。
-
非必要的UI视图刷新,如只修改局部组件状态时导致组件所在页面的整体刷新。
1、合理选择装饰器
最小化状态共享范围
在没有强烈的业务需求下,尽可能根据组件颗粒度,将状态分为组件内独享的状态和组件间需要共享的状态。
-
组件内独享的状态
常用@State装饰器,组件内独享的状态的生命周期和组件同步,状态的定义和更新都在组件内,组件销毁,状态也随即消失。
-
组件间需要共享的状态
三种场景:父子组件间共享状态,不同子树上组件间共享状态和不同组件树间共享状态。
应优先选择共享范围能力小的装饰器方案,减少不同模块间的数据耦合,便于状态及时回收。建议选择装饰器的优先级为:@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink > @Provide+@Consume > LocalStorage > AppStorage。
减少不必要的参数层层传递
由于@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink三种方案的实现方式是逐级向下传递状态,当共享状态的组件间层级相差较大时,会出现状态层层传递的现象。对于没有使用该状态的中间组件而言,这是“额外的消耗”,不利于代码的维护和拓展。
当共享状态的组件间跨层级较深时,或共享的信息对于整个组件树是“全局”的存在时,选择@Provide+@Consume的装饰器组合代替层层传递的方式,能够提升代码的可维护性和可拓展性。
按照状态复杂度选择装饰器
-
观察嵌套类对象的深层属性变化的场景,选择@State+@Observed+@ObjectLink。
-
状态是复杂对象、类或其类型数组的场景,选择@State+@Link。
-
状态是简单数据类型时,使用@State+@Link和@State+@Prop均可。在功能层面上,依据@Prop单向绑定的特性,@State+@Prop适合用于非实时修改的场景,如编辑电话薄联系人信息时,展示编辑界面的子组件信息的修改要求不实时同步回父组件,需要等到编辑完成后点击“确认”按钮时才会以事件驱动的方式修改父组件的状态。依据@Link双向绑定的特性,@State+@Link适合用于实时修改的场景,如组件嵌套时的滚动条同步。
2、精细化拆分复杂状态
AppStorage作用范围最广,但是ArkUI会以整个对象颗粒度通知所有使用了该状态的组件重新渲染,而不是按属性颗粒度大小通知使用了该变化属性的组件重新渲染。
从性能的角度考虑,在使用LocalStorage或AppStorage装饰器存储状态变量时需要合理设计状态的数据结构,避免无意义的渲染刷新。
3、集中化状态修改逻辑
在使用@Link装饰器时,开发者可以直接在@Link装饰器接收状态的组件内部修改状态。当多个子组件修改状态的逻辑基本相同时,建议将状态的修改集中到单个函数中,以提升逻辑的可复用性、代码的可维护性和可测试性。
4、总结
状态管理是MVVM模式中十分复杂的问题,为解决其中状态和视图一致性、渲染性能体验、代码可复用性和可维护性四个问题,本文主要有以下建议点:
-
在选择装饰器时,应理解各个装饰器的特性和共享范围,结合实际开发场景的优先级,合理选择装饰器,以确保状态和视图的一致性。
-
在使用装饰器时,对装饰器修饰的复杂变量应进行合理拆分设计,以此减少非必要的组件渲染次数,获得更好的性能体验。
-
在代码开发过程中,对相似的逻辑处理,应考虑其复用性合理集中处理,以此有效提升代码的可维护性和可复用性。
三、合理布局
1、精简节点数
布局阶段是采用递归遍历所有节点的方式进行组件位置和大小的计算, 如果嵌套层级过深,将带来了更多的中间节点,在布局测算阶段下,额外的节点数将导致更多的计算过程,造成性能消耗。
移除冗余节点:对于常出现冗余的情况,例如可能会在Row容器包含一个同样也是Row容器的子级。这种嵌套实际是多余的,并且会给布局层次结构造成不必要的开销。
使用扁平化布局减少节点数:在某些情况下,开发者所实现的布局在嵌套层级上是没有冗余的,但是嵌套层级仍然较深,可能无法通过调整现有的布局方案,可以通过切换到完全不同的布局类型来实现层次结构的扁平化。而扁平化布局是一种让页面结构变浅变宽的方式,通过一些高级组件如RelativeContainer、Grid等容器,可以让元素在平面上展开。这种布局方式能够有效减少由于使用线性布局带来的嵌套深度,将其用于描述布局的容器节点进行优化,达到精简节点数的目的。
2、利用布局边界减少布局计算
对于组件的宽高不需要自适应的情况下,建议在UI描述时给定组件的宽高数值,当其组件外部的容器尺寸发生变化时,例如拖拽缩放等场景下,如果组件本身的宽高是固定的,理论上来讲,该组件在布局阶段不会参与Measure阶段,其节点中保存了对应的大小信息,如果组件内容较多时,由于避免了其中组件整体的测算过程,性能会带来较大的提升。
3、合理使用渲染控制语法
合理控制元素显示与隐藏:用Visibility.None、if条件判断等都能够实现该效果。其中if条件判断控制的是组件的创建、布局阶段,visibility属性控制的是元素在布局阶段是否参与布局渲染。
对于不同的场景下,需要选择合适的手段,根据性能或者内存要求选择不同的实现方式:
-
只有初始的一次渲染或者交互次数很少的情况下,建议使用if条件判断来控制元素的显示与隐藏效果,对于内存有较大提升;
-
如果会频繁响应显示与隐藏的交互效果,建议使用切换Visibility.None和Visibility.Visible来控制元素显示与隐藏,提高性能;
4、合理使用布局组件
选择合适的布局组件:在布局时,子组件会根据父组件的布局算法得到相应的排列规则,然后按照规则进行子组件位置的摆放。不同的布局容器使用的布局算法对性能带来的影响不同。开发者应该根据场景选用合适的布局,除非必须,尽量减少使用性能差的布局组件。
Scroll嵌套List场景下,给定List组件宽高:在使用Scroll容器组件嵌套List组件加载长列表时,若不指定List的宽高尺寸,则默认全部加载。
Scroll嵌套List时:
List没有设置宽高时,List的所有子组件ListItem都会参与布局。
List设置宽高,只有布局区域内的ListItem子组件会参与布局。
List使用ForEach加载子组件时,无论是否设置List的宽高,都会加载所有子组件。
List使用LazyForEach加载子组件时,没有设置List的宽高,会加载所有子组件,设置了List的宽高,会加载List显示区域内的子组件。
5、组件嵌套优化
ArkUI框架的执行顺序:
执行ArKTS中的UI描述信息,通过UI描述API创建后端的页面节点树FrameNode树,其中包含了处理UI组件属性更新、布局测算、事件处理等业务逻辑;
通过FrameNode生成当前的界面描述数据结构——渲染树RenderTree,RenderTree描述了具体的元素在屏幕上的布局信息,包含大小、位置以及一些其他属性;
最后渲染线程会根据RenderTree的信息执行相应的绘制工作。
从ArkUI框架的执行流程上分析,组件的过度嵌套会导致节点树深度过高,由于对节点树的遍历方式为深度优先遍历,所以节点树深度过高则会影响性能,节点树应更更趋向于扁平化。优化方法同前面扁平化与合理使用布局组件。
四、合理动画
1、提升动画感知流畅度
基于视觉效果设计,我们可以将动效划分为特征动效、转场动效、手势动效、微动效、插画动效。在特征动效中呈现出天体运动“力”的即视感;转场动效表现出物体在运动过程中“力”的秩序感;手势动效打造出元素运动互相影响“力”的控制感;微动效和插画动效辅助HarmonyOS动效引力体系,增加用户的操作趣味性和浏览愉悦感。
动效场景设计
特征动效:特征动效主要打造 “天体拟物感知”,提供一种天体拟物的品牌效应和宇宙空间感的交互体验,它将力赋予元素,更直观地传递出形象化、拟物化、动态化的设计理念,在不同场景上表达新颖个性的同时又凸显了独特的产品调性。
转场动效:指在不同页面或视图之间切换时使用的动画效果。通过转场动效,可以平滑地过渡到下一个页面或视图,增加界面间的连贯性和流畅性。
手势动效:根据用户的手势操作而产生的动画效果。通过手势动效,可以增强用户与设备之间的互动体验。我们主张无阻塞感的动效设计,结合运用 HarmonyOS 动效物理引擎,将自然属性运用到界面的操作中,比如摩擦力、弹性、碰撞影响等。
微动效:指在界面中细微的动画效果,用于增加界面的生动感和交互性。微动效可以体现在按钮的点击效果、图标的变化、文本的出现等。
插画动效:指在界面中应用的基于插画的动画效果。通过插画动效,可以为界面增添趣味和个性化。例如,在一个游戏应用中,可以使用插画动效来展示角色的动作、表情或者场景的变化。
动画能力选型
开发人员接收到设计需求后,需要选择合适的动画能力完成该设计。HarmonyOS为开发者提供了系统能力、资源调用、三方库三种方式。
系统能力:属性动画、显示动画、转场动画、粒子动画、路径动画
资源调用:GIF动画、帧动画
三方库:Lottie、SVG
2、提升动画运行流畅度
通过以下优化手段的单个使用或组合使用,对动画帧率、应用卡顿等方面带来优化,提升性能和用户体验。
使用系统提供的动画接口:系统接口经过精心设计和优化,能够在不同设备上提供流畅的动画效果,最大程度地减少丢帧率和卡顿现象。
使用图形变换属性变化组件布局:通过对组件的图形变换属性进行调整,而不是直接修改组件的布局属性,可以减少不必要的布局计算和重绘操作,从而降低丢帧率,提升动画的流畅度和响应速度。
参数相同时使用同一个animateTo:当多个动画的参数相同时,合并它们并使用同一个animateTo方法进行处理能够有效减少不必要的计算和渲染开销。
多次animateTo时统一更新状态变量:在进行多次动画操作时,统一更新状态变量可以避免不必要的状态更新和重复渲染,从而减少性能开销。
五、合理转场
通过以下步骤,开发者可以将UX设计视角转换为开发实现视角,并将设计师提供的转场场景和动效转化为具体的代码实现。这样可以确保应用在实际使用中达到设计的预期效果,并提供良好的用户体验。
了解系统能力:开发者需要深入了解HarmonyOS系统提供的转场能力和动画功能,包括了解页面转场的机制、转场动画的类型和特性,以及如何在HarmonyOS应用中使用相关API。
分析UX设计视角:仔细分析UX设计所提供的转场场景和动效。理解设计师的意图,包括页面之间的过渡效果、元素的动画行为,以及何时触发和结束转场动效。
设计转场方案:基于分析的结果,设计出合理的转场方案。确定页面切换的触发时机、转场动画的类型和参数等。
使用转场能力:利用HarmonyOS提供的转场能力,如UIAbility转场、页面路由转场、组件转场,来实现页面之间的转场效果。
使用动画能力:利用HarmonyOS提供的动画能力,如属性动画、出现/消失转场动画等来实现元素的平移、缩放、旋转、透明度等动画行为。
(可选)使用高级模板化转场:HarmonyOS已经为我们提供了一些基于场景化封装的相关高级模板化转场,如导航转场、模态转场、共享元素转场。
调试和优化:在实施转场和动效的过程中,进行调试和优化。确保转场效果流畅,动效符合预期,且满足性能要求。
1、转场场景设计
转场动效:左右位移遮罩动效、左右间隔位移动效、一镜到底动效、淡入淡出动效、缩放动效
转场场景:
-
层级转场:指在用户界面中,从一个层级结构的界面状态转换到另一个层级结构的界面状态的过程,它通常用于在应用中进行页面间的导航和视图层级的变化。
-
搜索转场:指在用户执行搜索操作,如在搜索栏中输入关键词并按下搜索按钮、或者直接触摸搜索图标时,应用改变应用页面以显示搜索结果的过程。
-
新建转场:指用户创建新内容或实体时,应用页面发生的过渡效果,它可以让用户感知到新的事物的添加或创建,并提供一种连贯和引人注目的视觉切换。
-
编辑转场:用户对现有内容或实体进行编辑时,例如点击“编辑”按钮、选择要编辑的项目或内容,或者执行其他与编辑相关的动作,应用应提供动效引导用户进入一个用于编辑现有内容的页面,修改所需的信息。
-
通用转场:通用转场是一种广泛适用于不同情境和应用类型的页面过渡效果,目的是提供一种通用的、可重复使用的方式,以改善用户页面之间的切换,增强用户体验。
-
跨应用转场:指用户从一个应用程序切换到另一个应用程序,用户能够无缝地从一个应用切换到另一个应用,而不会感到中断或不适。
2、转场场景开发
转场能力
HarmonyOS为开发者提供了UIAbility转场、页面路由和组件转场三种方式,在选择转场方式时,开发者需要考虑用户体验、界面一致性和业务需求,确保所选导航组件能够提供直观、易用的导航方式,帮助应用实现更好的转场效果。
UIAbility组件间交互:UIAbility是系统调度的最小单元。通过调用startAbility()方法启动UIAbility实现在设备内的功能模块之间的跳转。该UIAbility可以是应用内的其他UIAbility,也可以是其他应用的UIAbility(例如启动三方支付UIAbility)。
页面路由(@ohos.router):页面路由指在应用程序中实现不同页面之间的跳转和数据传递。HarmonyOS提供了Router模块,通过不同的url地址,可以方便地进行页面路由,轻松地访问不同的页面。
组件转场:组件转场是指通过HarmonyOS提供的各类组件来实现转场效果,以便更加便捷地展示不同的内容或功能模块。在组件转场中,可以使用诸如页面切换动画、过渡效果、布局变化等方式来实现页面之间的平滑切换。
动画能力
-
导航转场:页面的路由转场方式,对应一个界面消失,另外一个界面出现的动画效果,如设置应用一级菜单切换到二级界面。
-
模态转场:新的界面覆盖在旧的界面之上的动画,旧的界面不消失,新的界面出现,如弹框就是典型的模态转场动画。
-
共享元素转场:共享元素转场是一种界面切换时对相同或者相似的元素做的一种位置和大小匹配的过渡动画效果。
六、懒加载
1、渲染过程
ForEach:
-
从列表数据源一次性加载全量数据。
-
为列表数据的每一个元素都创建对应的组件,并全部挂载在组件树上。即,ForEach遍历多少个列表元素,就创建多少个ListItem组件节点并依次挂载在List组件树根节点上。
-
列表内容显示时,只渲染屏幕可视区内的ListItem组件,可视区外的ListItem组件滑动进入屏幕内时,因为已经完成了数据加载和组件创建挂载,直接渲染即可。
数据懒加载
LazyForEach:
-
LazyForEach会根据屏幕可视区能够容纳显示的组件数量按需加载数据。
-
根据加载的数据量创建组件,挂载在组件树上,构建出一棵短小的组件树。即,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。
-
屏幕可视区只展示部分组件。当可视区外的组件需要在屏幕内显示时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。
2、常用场景
长列表加载:长列表作为应用开发中最常见的开发场景之一,通常会包含成千上万个列表项,在此场景下,直接使用循环渲染ForEach一次性加载所有的列表项,会导致渲染时间过长,影响用户体验。而使用数据懒加载LazyForEach替换循环渲染ForEach,可以按需加载列表项,从而提升列表性能。
无限瀑布流:瀑布流的内容呈现方式类似瀑布流一样,从上往下依次排列,每一列的高度不一定相同,整体呈现出瀑布流的视觉效果。在瀑布流中,经常使用LazyForEach实现数据按需加载,同时,结合onReachEnd、onApear方法实现无限瀑布流,
3、常见失效场景
键值相同导致渲染错乱:在LazyForEach的键值生成规则中,每个item对应着一个唯一且持久的键值,用于标识对应的组件。当不同的数据项有相同的键值时,框架可能找不到正确的数据项,导致子组件渲染错误。
ListItem过于复杂导致丢帧:当使用LazyForEach时,如果子组件ListItem过于复杂,在子组件创建时,将产生大量的布局计算耗时,最终导致该帧丢帧。
Scroll嵌套List导致按需加载失效:当Scroll容器嵌套List组件加载长列表时,若不指定List的宽高尺寸,则默认加载全部ListItem,导致按需加载失效,甚至会导致应用卡顿、崩溃。
GridItem未设置高度导致按需加载失效:当使用Grid容器时,如果GridItem没有设置高度,会加载所有子组件,设置了GridItem的宽高,会加载Grid显示区域内的子组件。
更多详情请查看:文档中心