HarmonyOS:点击响应优化
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤博客园地址:为敢技术(https://www.cnblogs.com/strengthen/ )
➤GitHub地址:https://github.com/strengthen
➤原文地址:https://www.cnblogs.com/strengthen/p/18517809
➤如果链接不是为敢技术的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
响应优化概述
响应(Response)是指应用在运行中,由用户直接或间接做出一个行为请求,当程序接受了该请求,随即做出一系列运算,最终界面发生变化的过程。简单来说,即用户发出指令,程序执行,设备显示变化。
在应用生态高速发展的背景下,虽然移动设备的硬件运算性能已经达到了新的高度,但与此同时应用研发者也设计出更加多元化、智能化的产品,展现样式百花齐放,这些产品对高性能的需求与日俱增,加上同类型相近功能的产品互相竞争,用户也会对App产品质量的要求越来越高,对响应速度的要求也愈加严格。
《应用性能体验建议》指出,应用或元服务内点击操作响应时延应<=100ms。为了保障应用操作响应及时,看护用户极致流畅体验,开发者需要分析从手势抬手到渲染上屏这段时间应用做了哪些耗时的操作,进而针对性地优化相关逻辑。
点击响应优化就是指通过分析响应阶段、优化应用性能、加快点击后页面的响应速度,使用户可以得到流畅的操作体验。开发者优化自己应用的点击响应速度,既满足产品功能的高性能要求,增强产品同质化竞争中的优势,又能不断提升用户满意度。
仔细观察响应过程中涉及的内容,可以发现优化的目标集中在:UI界面、视觉动效、指令逻辑等。本文将围绕这些元素,结合平台的相关特性,介绍点击响应优化的具体方法:
- UI优化:从UI布局渲染角度,加快应用绘制性能,比如减少布局嵌套,减少元素渲染,缓存UI动效等。
- 按需加载优化:根据界面展示或模块加载的需要,延迟加载相关内容,从而减少对首帧页面的性能消耗。
- 并发优化:使用系统并发执行多个任务的能力,减少响应过程中任务执行的整体时间。
- IPC通信优化:针对IPC通信特点,尽量减少不必要的通信次数和数据体积,防止IPC通道影响界面响应。
- 代码逻辑优化:在相关生命周期中减少冗余、降低耗时,提升执行效率,包括善用数据结构、缓存、优化调整时序等。
- 视觉感知优化:通过交互设计的优化,利用动效动画的形式,在视觉层面提升用户响应速度的感知。
分析工具
影响点击响应性能的因素有很多,借助DevEco Studio集成的相关分析工具,可以收集大量的系统数据,自动执行重复任务,建立统一的优化标准和流程,减少个人差异和误操作的可能性,帮助开发人员更好地了解性能瓶颈和优化潜力。在分析优化的过程中,可能用到以下工具中的一个或多个。
AppAnalyzer
AppAnalyzer是DevEco Studio中提供的检测评分工具,用于测试并评价HarmonyOS应用或元服务的质量,能快速提供评估结果和改进建议,当前支持的测试类型包括兼容性、性能、UX测试和最佳实践等,其中点击操作响应是性能类型中的一项检测规则,开发者可以使用该工具检测响应性能。
使用AppAnalyzer检测点击响应
- 启动DevEco Studio,连接设备,打开应用,然后依次执行以下操作。
- 单击菜单栏Tools > AppAnalyzer。
- 在AppAnalyzer页面Module选择框选择应用/服务工程模块。
- 根据应用的类别选择Category。
- 选择Rules,可以先点击Custom,再勾选”Quick Response To In-app Clicks”,即勾选”应用内点击操作响应快”评测规则。
- 获得检测结果。例如下图结果,表示点击响应性能检测失败,说明评测应用达不到100ms响应标准,存在进一步优化的空间。
具体使用可参考《应用与服务体检》。
Profiler Frame
Profiler是DevEco Studio提供的场景化调优工具,其中Frame可以帮助开发者深度分析性能问题,通过录制应用运行过程中的关键数据,从而识别卡顿丢帧、耗时长等问题的原因所在。
使用Frame分析响应性能
- 抓取操作trace:启动DevEco Studio,连接设备,打开应用,然后依次执行以下操作。
- 启动Profiler。
- 选择应用、包名、进程。
- 选择Frame工具。
- 操作到指定页面,点击Create Session 创建一个Frame 模板。
- 点击Frame模板框中的播放按钮开始录制,操作应用界面点击响应,完成操作之后点击结束录制。
- 确认响应起点和终点:分解时间段:点击响应的整体时延拆解后,主要分为三个阶段:输入阶段,应用阶段,渲染阶段。
- 根据点击响应的初始位置,找到手势抬起的那一帧,设置为分析起点,对应mmi-service泳道中H:service report的type为up的事件。
- 找到页面变化后的第一帧,设定为分析的终点,对应RSHardwareThread泳道的CommitAndReleaseLayers结束点。
-
首帧响应时延
起点
终点
基线(ms)
输入阶段
mmi_service对应的service report(type为up)
应用DispatchTouchEvent的起点(type=1)
8
应用阶段
应用DispatchTouchEvent的起点(type=1)
页面首次发生变化帧对应的H:FlushMessage结束点
25
渲染阶段
对应的RS帧ProcessCommandUni起点
对应的RSHardwareThread::CommitAndReleaseLayers的结束点
20
其中,应用阶段(如下图中标记2与3之间的部分)是开发者需要关注优化的部分,一般来说,应用阶段耗时若超过25ms的基线,加上机器硬件的30ms左右耗时,整体的时延就可能超过100ms的标准,导致点击响应体验不佳,需要进一步定位性能问题。
- 分析定位原因:针对框选的应用阶段,分析主进程泳道,观察是否有耗时长的函数阻塞主线程;是否有超长耗时单帧。如果有长段的ExecuteJs 可以查看具体的调用栈或火焰图,定位耗时函数;如果是FlushLayoutTask阶段耗时,就需要结合UI组件树去分析,布局是否合理,是否有优化空间。
更多使用方法参考《Frame分析》。
ArkUI Inspector
ArkUI Inspector是DevEco Studio中提供用于检查UI的工具,开发者可以借助它预览真机或模拟器中的UI效果,快速定位布局层级问题,也可以观察组件属性、不同组件之间的关系等。
具体的使用场景和操作参考《布局分析》。
UI优化
原理介绍
应用开发中的用户界面UI(User Interface)是用户与应用程序交互的关键部分。使用不同类型的布局,能在界面显示上达到预期效果,甚至某些方式能将页面排布得更加美观,但也容易引入不合理的结构问题,如果UI界面中存在过度的布局计算,冗余的元素绘制,将会带来设备资源的大量开销,造成响应性能的衰退。
减少嵌套层级
布局的嵌套层次过深会导致在创建节点及进行布局时耗费更多时间。因此开发者在开发时,应避免冗余的嵌套,并尽量使用扁平化布局来优化嵌套层级。
减少渲染时间
if/else条件渲染是ArkUI应用开发框架提供的渲染控制能力,可根据应用的不同状态,渲染对应分支下的UI描述。
具体内容见合理使用渲染控制语法。
用renderGroup缓存动效
页面响应时,可能大量使用属性动画和转场动画,当复杂度达到一定程度之后,就有可能出现卡顿的情况。renderGroup是组件通用方法,它代表了渲染绘制的一个组合。
具体原理是在首次绘制组件时,若组件被标记为启用renderGroup状态,将对组件及其子组件进行离屏绘制,将绘制结果合并保存到缓存中。此后当需要重新绘制相同组件时,就会优先使用缓存而不必重新绘制了,从而降低绘制负载,进而加快响应速度。
为了能使renderGroup功能生效,有以下限制条件:
- 组件内容固定不变:组件及其子组件各属性保持固定,不发生变化。如果组件内容不是固定的,也就是说其子组件中存在某些属性变化或者样式变化,此时如果使用renderGroup,那么缓存的利用率将大大下降,并且有可能需要不断执行缓存更新逻辑,在这种情况下,不仅不能优化卡顿效果,甚至还可能使卡顿恶化。例如:文本内容使用双向绑定的动态数据;图片资源使用gif格式;使用video组件播放视频。
- 子组件无动效:由组件统一应用动效,其子组件均无动效。如果子组件上也应用动效,那么子组件相对父组件就不再是静止的,每一帧都有可能需要更新缓存,更新逻辑同样需要消耗系统资源。
场景案例
本节的示例是一个应用开发中常见的留言箱列表。
从设计图稿中可看出,列表视图中每一个子项都包含了头像、消息红点、昵称、最新信息、时间等元素。
应用进入到该页面时,将根据每条消息的元素数据,呈现出不同的样式内容。
分解其中的关系结构,单个子项界面的构成元素主要有6个,元素排列以线性风格为主,可能使用到的组件有Image,Badge,Text等。
具体实现
- 熟悉弹性布局的开发者,得到需求后往往会第一时间使用Flex容器去实现:界面主体上呈线性结构,使用Flex作为包裹父容器,依次将横向纵向的元素添加进去,并设置其样式属性。通过DevEco Studio内置ArkUI Inspector工具,可以得到布局代码对应的视图树,实现结果从根结点到元素叶子结点最深处有6层(如图5所示),组件数目为15。这种实现方式,由于Flex容器默认情况下存在shrink等行为,绘制时会二次布局,会一定程度上造成页面渲染上的性能劣化。
- 接下来采用相对布局去优化实现,先将左侧头像添加到容器中,然后锚定它的位置,逐一在右侧添加其他元素。实现结果使用工具观察,发现层级相对减少,最终实现的层级是3层(如图6所示);同时借助相对布局,子元素结构扁平化,容器也相对减少,进一步优化了页面的构建渲染时间。
统计分析
在不同布局下,相同界面效果所需要的组件数目和嵌套数目是完全不同的,对本案例场景下的留言箱进行拆分可以得到以下数目对比:
组件数目 |
嵌套数目 | |
---|---|---|
Flex实现方式 |
15 |
6 |
相对布局实现方式 |
9 |
3 |
分别使用相同数目的数据测试,通过记录从上一页面点击启动到留言箱列表渲染为止的响应时延,得到其对比结果,如下表:
256条数据 |
512条数据 |
1024条数据 | |
---|---|---|---|
Flex实现方式 |
308ms |
570ms |
1096ms |
相对布局实现方式 |
249ms |
499ms |
986ms |
优化百分比 |
19.2% |
12.5% |
10.0% |
从最后统计的数据来看:
- 在这种形态相对单一的案例场景中,响应时长明显降低50ms以上,优化比也得到了10%以上的速度提升。说明减少组件渲染数目、减少布局嵌套层级,有助于提升应用UI的绘制性能,可以一定程度上减少响应时延。
- 另一方面,随着加载数据量的提升,优化时长占原有响应绝对值的占比在下降,说明列表在较大数据的加载过程中,存在其他性能瓶颈,还可以结合更多的优化方法去提升性能表现。(注:本例采用了ForEach列表实现方式。)
在更为复杂的实际开发场景中,简化布局结构、精简相关元素(包括绘制的子元素及其父容器),决定了响应速度在UI层面的潜在提升空间。
按需加载优化
LazyForEach懒加载
使用LazyForEach懒加载替换ForEach,避免像ForEach那样一次性初始化和加载所有元素,从而使首帧绘制时创建列表元素时间大大减少,提升响应性能表现。
相关原理及案例参考《长列表加载性能优化》。
动态import
动态import是一种模块加载机制,允许应用程序在运行时按照实际需求去加载相关模块。在某些条件满足时(比如用户交互时,或ABTest分支切换时)再加载特定模块,可以减少初始化import的加载时间和资源消耗,这将有助于提高应用程序的内存性能和响应速度。
与静态import不同,动态import仅在需要时才消耗CPU等资源;相比静态import在编译时就确定了引入的所有模块,动态import还有更佳的语法灵活性,借助这种灵活,能够实现代码和路由级别的粒度分割,优化模块层次的懒加载性能。
具体的使用场景和实现方案参考《动态import》。
并发优化
原理介绍
并发是指多个任务在同一个时间段内同时触发执行,具体逻辑中使用多线程异步执行,与之相对的概念是串行任务,按顺序同步执行。
应用中的并发优化就是在响应用户操作期间,尽可能地让主线程只执行UI绘制相关的任务,而将非UI的耗时任务分配给其他线程或者延迟处理。这样借助多线程的异步技术,充分利用多核处理器的能力,提高应用程序的并发处理能力,减少用户等待时间,保证用户界面的响应流畅性。
异步任务并发处理
- 利用TaskPool执行简单并行任务:将一些耗时的操作放入异步任务中处理,避免阻塞主线程,提升应用的响应速度;
- 利用Worker完成周期类耗时操作:Worker可以空跑在后台等待事件触发,周期触发耗时操作使用Worker,这样可以避免TaskPool频繁拉起影响性能。
二者原理和效果差异可参考《TaskPool和Worker的对比实践》。
使用组件异步加载特性
Image组件支持异步加载特性:当应用在页面上展示一批图片的时候,会先显示空白占位块,当图片在其他线程加载完毕后,再替换占位块。这样图片加载就可以不阻塞页面的显示,给用户带来良好的交互体验。
设置示例:
// syncLoad设置false,或缺省设置,都是异步加载图片 Image('https://example.com/icon.png') .syncLoad(false)
相应的,如果展示图片数目很少,或加载本地图片,耗时明显较少时,这时建议配置syncLoad属性为true,使图片同步加载,避免特定情况下图片加载出现的闪烁。
场景案例
该示例是一个添加地址的功能,当点击“选择“按钮后,应用响应切换到选择城市地区的目标页。在切换过程中,要将全国城市数据加载,根据首字符排序,然后刷新到新页面列表中。
在实际场景中,开发者可能直接用同步串行的方式实现这一功能:点击按钮后,初始化页面,将数据加载到内存中,然后渲染出整个界面视图。
当目标页选择范围到”市”这一行政级别,数据量大约1000条,新页面响应速度理想,直观上没有异样;
如果功能需求调整,当目标页选择范围到”区”这一级别,数据量达到2000,页面响应速度尚可接受;
如果功能进一步调整,当目标页选择范围到”乡镇/街道”这一级别时,数据量突破4000,页面响应出现明显延迟。
因为地区数据的加载和排序,这两个过程都需要消耗一定的性能计算,在这种情况下,可以使用并发机制来优化。
下图是加载4000条数据(图中因功能需要先选择”城市”,但包含”乡镇/街道”的数据在页面进入时都已同时加载),优化前后,同时点击”选择”按钮时的效果展示:
代码实现
在目标页面的aboutToAppear中,使用TaskPool启动子线程加载城市数据,实现并发操作:
@Concurrent function computeTask(): string[] { let array: string[] = [] // AppConstant.CITYS 为需要加载的数据 for (let t of AppConstant.CITYS) { array.push(t.trim()) } let collator = new Intl.Collator("zh-CN", { localeMatcher: "lookup", usage: "sort" }); array.sort((a, b) => collator.compare(a, b)) return array; } @Entry @Component struct CityList { isAsync: boolean = (router.getParams() as Record<string, boolean>)['isAsync']; // 界面数据 @State citys: string[] = [] private listScroller: Scroller = new Scroller(); aboutToAppear() { this.computeTaskAsync(); // 调用异步运算函数 } // 异步线程 computeTaskAsync() { let task: taskpool.Task = new taskpool.Task(computeTask); taskpool.execute(task).then((res) => { this.citys = res as string[] }) } // ... }
统计分析
使用Profiler Time工具,分别测试不同数量级别下的响应时长,得到结果如下:
500条数据 |
1000条数据 |
2000条数据 |
4000条数据 | |
---|---|---|---|---|
串行 |
49ms |
94ms |
296ms |
780ms |
并发 |
48ms |
86ms |
140ms |
172ms |
可以看出,在该场景下,如果数据量不大(小于1000),串行加载的用户体验也可以保持良好,但随着城市数据量的递增,当超过1000条时,响应时长增大加快,体验开始显著恶化,从手指抬起到页面转场进入列表页的第一帧画面,会明显出现视觉可见的响应迟滞。
这时采用并发异步加载的优势体现出来,在实践中,UI主线程可以第一时间拉起目标列表页面,同时触发异步加载及排序的逻辑线程,待结果返回后再刷新到列表中,提升整体的响应速度。
减少调用数据库API次数
原理介绍
数据库是存储和管理数据的工具集合。通过系统框架封装的API,可以对数据及其结构进行访问、管理、增添或更新等一系列操作。
通常来讲,数据库的调用会比较耗时,常见的增、删、改、查等都提供了异步接口,合理使用这些接口不会对响应性能产生影响。
然而一些同步API,因为调用频率较高,反而成为响应时的性能瓶颈。
比如:ResultSet的getColumnIndex,其作用是根据指定的列名获取列索引,getColumnIndex执行的耗时大约只是几十微秒的量级,但如果在循环查询数据条目时,每次都重复调用了getColumnIndex,最终的耗时=数据条数*获取索引耗时,当查询数据量大的表时,总开销并不低。
那么优化的方法就是减少调用该API的次数。因为同一数据表在结构确定后,相应字段的index索引不会变化 ,所以提前缓存index索引值,将getColumnIndex调用提取到代码循环体外,避免重复调用,可以减少耗时。
场景案例
本节示例是一个记账工具应用,其基于关系型数据库管理相关账目。
在查询用户数据时,会依次读取account表中每一行的数据,其中每一列column的值,需要借助getColumnIndex("列名")得到column索引,然后再取得对应值 。
修改前代码:
for (let i = 0; i < count; i++) { let tmp: AccountData = { id: 0, accountType: 0, typeText: '', amount: 0 }; tmp.id = resultSet.getDouble(resultSet.getColumnIndex('id')); tmp.accountType = resultSet.getDouble(resultSet.getColumnIndex('accountType')); tmp.typeText = resultSet.getString(resultSet.getColumnIndex('typeText')); tmp.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')); result[i] = tmp; resultSet.goToNextRow(); }
从代码可知,for循环内调用getColumnIndex,总的调用次数会随着数据行数count的增大而增大。数据表结构固定的情况下,索引不会变化 ,可以将调用提前以减少总的调用次数,优化指令耗时。
修改后代码:
const idIndex = resultSet.getColumnIndex("id"); const accountTypeIndex = resultSet.getColumnIndex("accountType"); const typeTextIndex = resultSet.getColumnIndex("typeText"); const amountIndex = resultSet.getColumnIndex("amount"); for (let i = 0; i < count; i++) { let tmp: AccountData = { id: 0, accountType: 0, typeText: '', amount: 0 }; tmp.id = resultSet.getDouble(idIndex); tmp.accountType = resultSet.getDouble(accountTypeIndex); tmp.typeText = resultSet.getString(typeTextIndex); tmp.amount = resultSet.getDouble(amountIndex); result[i] = tmp; resultSet.goToNextRow(); }
统计分析
使用Profiler Time工具,分别测试不同数量下的响应时长,得到结果如下:
50条数据 |
500条数据 |
5000条数据 | |
---|---|---|---|
修改前 |
72ms |
97ms |
157ms |
修改后 |
72ms |
92ms |
110ms |
由此可以看出,在数据库使用时,要关注相关API调用的潜在频率:
数据条目在初期量级不大时,API调用行对应用响应影响甚微;伴随着使用时间的增长,数据量级大到一定程度,这种潜在可能的高频调用,会直接影响程序的性能表现。
IPC通信阻塞优化
原理介绍
操作系统提供IPC(Inter-Process Communication,进程间通信)机制进行跨进程通信。需要跨进程通信的原因是因为每个进程都有自己独立的资源和内存空间,其他进程不能随意访问不同进程的内存和资源。当工作进程需要获取其他进程的数据或能力时,就要使用IPC的通信能力。而IPC过程会消耗一定的系统资源 ,受限于实现底层机制,单位时间内传输的数据量是受限的,短时间内频繁IPC,有可能产生通信阻塞。
目前系统有三种情况应用了IPC的通信能力:
- 调用涉及IPC的API,比如WIFI、蓝牙、NFC等模块中的同步接口。
- 使用@ohos.rpc模块开发实现跨进程通信的能力。
- 后台服务:通过IPC机制提供跨进程的服务调用能力,实现一个后台服务并对外提供相应的能力。
通常可以从以下几个方面进行优化。
本地缓存数据:合理使用本地缓存,将常用的数据存储在本地,而不是每次都通过IPC请求。这样可以减少不必要的通信次数。确保缓存数据的有效期和更新策略是合理的,以防止缓存数据过期或不一致。
批处理请求:将多个IPC请求合并成一个批处理请求,从而减少通信的次数。这对于频繁的小数据请求尤其有用。
异步处理:对于不需要立即响应的IPC请求,可以将它们设置为异步处理,以免阻塞主线程。
减少不必要的数据传输:确保只传输应用所需的数据,避免传输不必要的信息。
场景案例
本节场景是一个Wifi热点列表的目标页面。
当用户从上一个页面点击进入,获取扫描结果的API是WifiManager.getScanInfoList,通过Wifi管理器进程得到,是典型的IPC通信,并且getScanInfoList作为同步接口,会阻塞主线程,单次访问目标页面的响应有一定耗时;另一方面,如果用户未等待结果显示出来,返回上一页面,再次进入,如此反复操作,IPC频繁调用后,其通信管道受阻,响应耗时逐渐增大。
![点击放大](https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20241030161935.04821604896572178094974506858545:50001231000000:2800:C262D15AF6267BD832190FC928DCC1ED9B0A436B44E3623FB684A90D35F5375A.gif?needInitFileName=true?needInitFileName=true)
具体实现
针对该场景,结合IPC优化的常用方法,采取了以下措施:
- 修改同步接口为异步调用,缩短进入目标页面的响应时间。
- 缓存结果数据,优先显示缓存;设定缓存有效时间(比如3s),忽略有效时间内的重复请求,减少IPC阻塞的机率。
- 目标页面提供刷新按钮,从产品形态上给予用户控制能力,避免缓存失效不符合用户预期的情况。
代码逻辑优化
原理介绍
代码逻辑的优劣对应用响应速度的影响是比较明显的,特别是点击切换后新页面中的aboutToAppear、onPageShow等生命周期回调,以及点击操作页面中的aboutToDisappear等,需要充分优化代码、减少冗余、避免耗时,提升执行效率。
基于平台SDK的开发框架下,对App生命周期的理解,可以帮助开发人员识别程序在不同阶段的行为,弄清楚不同形态转换时触发的接口性质、各函数被调用的频率,进而挖掘出代码优化的方向。
下图是页面及自定义组件的生命周期流程:
通常可以采用的逻辑优化方法有:
- 选择合适的数据结构
索引存取考虑使用array数组,hash查找考虑使用map,去重逻辑考虑使用set等;
有时开发者使用object变量作为容器去处理map的逻辑,可以考虑使用ArkTS提供的高性能容器类,直接使用HashMap;
遇到纯数值计算的场合,推荐使用TypedArray的数据结构,比如Int8Array、Int32Array、Float32Array、BigInt64Array等。
- 合理使用缓存
当某些运算结果会反复使用时,以空间换时间,提前缓存以便于下次调用。
- 注意对象new和delete的频率
new和delete可能会触发内存管理回收,占用CPU资源从而影响界面渲染的能力,需要根据情况调整其频次。尤其在循环代码中,频繁的new、delete更会带来恶化的性能表现,应该尽量将new/delete优化到循环外去处理。
- 延迟执行资源释放操作
将资源关闭和释放操作放在setTimeout函数中执行,使其延迟到系统相对空闲的时刻进行,可以避免在程序忙碌时段占用关键资源,提升整体性能及响应能力。
- 减小拖动识别距离
应用识别拖动手势事件时需要设置合理的拖动距离,设置不合理的拖动距离会导致滑动不跟手、响应时延慢等问题。针对此类问题可以通过设置distance大小来解决。
场景案例:延迟执行资源释放操作
该场景是相机正常使用后,执行释放相机资源的相关操作。通过“停止拍摄进程 > 暂停并释放相机会话 > 关闭和释放预览及拍照的输入输出对象 > 清空相机管理对象”的过程,确保应用程序在不再使用相机时能够有效管理并回收所有相机资源。但是直接调用的release方法中captureSession、cameraInput、previewOutput、cameraOutput都用了await,使相机关闭和释放顺序执行可能会导致应用程序的响应性下降,造成用户界面卡顿。
下列代码将资源释放操作放在相机页面隐藏时触发的函数:
let cameraOutput: camera.PreviewOutput; let cameraInput: camera.CameraInput; let captureSession: camera.PhotoSession; let previewOutput: camera.PhotoOutput; // 相机页面每次隐藏时触发一次 onPageHide() { this.releaseCamera(); } // 释放资源 public async releaseCamera() { try { // 拍照模式会话类暂停 await captureSession?.stop(); // 拍照模式会话类释放 await captureSession?.release(); // 拍照输入对象类关闭 await cameraInput?.close(); // 预览输出对象类释放 await previewOutput?.release(); // 拍照输出对象类释放 await cameraOutput?.release(); } catch (e) { hilog.error(0x00, 'release input output error:', JSON.stringify(e)); } }
修改后,启动setTimeout异步延迟操作,在200ms后调用release释放关闭相机。其通过“停止拍摄进程 > 并发执行:(暂停并释放相机会话 > 关闭和释放预览及拍照的输入输出对象 > 清空相机管理对象)”的过程,确保应用程序在不再使用相机时能够有效管理并回收所有相机资源。移除await关键字应用于相机资源释放操作,允许异步并发执行,显著减少了主线程阻塞,从而提升了应用性能和响应速度。
let cameraOutput: camera.PreviewOutput; let cameraInput: camera.CameraInput; let captureSession: camera.PhotoSession; let previewOutput: camera.PhotoOutput; // 相机页面每次隐藏时触发一次 onPageHide() { setTimeout(this.releaseCamera, 200); } // 释放资源 public async releaseCamera() { try { // 拍照模式会话类暂停 await captureSession?.stop(); // 拍照模式会话类释放 await captureSession?.release(); // 拍照输入对象类关闭 await cameraInput?.close(); // 预览输出对象类释放 await previewOutput?.release(); // 拍照输出对象类释放 await cameraOutput?.release(); } catch (e) { hilog.error(0x00, 'release input output error:', JSON.stringify(e)); } }
性能比对
操作逻辑 |
trace图识别耗时 |
备注 |
---|---|---|
直接关闭与释放(修改前) |
457.5ms |
在onPageHide中直接执行相机关闭与释放操作 |
延时关闭与释放(修改后) |
85.6ms |
在onPageHide中使用setTimeout延迟200ms后执行关闭与释放操作 |
两组数据表明,合理运用延时策略能显著提升函数执行效率,是优化相机资源管理与关闭操作性能的有效手段,对提升整体用户体验具有重要价值。
场景案例:减小拖动识别距离
该场景是对一个组件添加手势事件。优化前,指定触发拖动手势事件的最小拖动距离为100vp,代码如下:
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit' @Entry @Component struct PanGestureExample { @State offsetX: number = 0 @State offsetY: number = 0 @State positionX: number = 0 @State positionY: number = 0 private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right }) build() { Column() { Column() { Text('PanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY) } .height(200) .width(300) .padding(20) .border({ width: 3 }) .margin(50) .translate({ x: this.offsetX, y: this.offsetY, z: 0 }) // 以组件左上角为坐标原点进行移动 // 左右拖动触发该手势事件 .gesture( PanGesture(this.panOption) .onActionStart((event: GestureEvent) => { console.info('Pan start') hiTraceMeter.startTrace("PanGesture", 1) }) .onActionUpdate((event: GestureEvent) => { if (event) { this.offsetX = this.positionX + event.offsetX this.offsetY = this.positionY + event.offsetY } }) .onActionEnd(() => { this.positionX = this.offsetX this.positionY = this.offsetY console.info('Pan end') hiTraceMeter.finishTrace("PanGesture", 1) }) ) Button('修改PanGesture触发条件') .onClick(() => { this.panOption.setDistance(100) }) } } }
利用Profiler工具分析得到trace图,其中主要关注两个trace tag分别是DispatchTouchEvent代表点击事件和PanGesture代表响应,追踪流程从应用侧的DispatchTouchEvent(type=0,标识手指接触屏幕)标签开始,到PanGesture(事件响应)的变化,该过程耗时145.1ms。
日志主要关注从应用接收TouchDown事件到pan识别耗时,该过程耗时127ms。(注:日志信息和trace图非同一时间获取,所获得的性能数据存在差异,提供的数值仅供参考。)
针对该组件,其拖动手势识别距离可以调整到更合适的数值,这里优化后,指定触发拖动手势事件的最小拖动距离为4vp,修改如下:
Button('修改PanGesture触发条件') .onClick(() => { this.panOption.setDistance(4) })
同样采用Profiler工具分析trace图,得到对应耗时38.4ms
对应日志过程耗时42ms。
性能比对
拖动距离设置 |
trace图识别耗时 |
日志识别耗时 |
备注 |
---|---|---|---|
最小拖动距离100vp(修改前) |
145.1ms |
127ms |
最小拖动距离过大会导致滑动脱手、响应时延慢等问题导致性能劣化 |
最小拖动距离4vp(修改后) |
38.4ms |
42ms |
设置合理的拖动距离优化性能 |
两组数据表明,合理减小拖动距离能显著提升执行效率,是优化响应时延的有效手段,对提升整体用户体验具有重要价值。(注:本案例通过设置较大和较小拖动距离进行数据对比得出相关结论。设置过小的distance容易出现误触等问题,开发者可根据具体应用场景进行设置。)
视觉感知优化
原理介绍
上述几节内容,是从减少时延绝对值的角度来提升响应体验,而视觉感知优化则是通过交互设计的优化,提升用户响应速度的感知。
从响应速度的反面角度来讲,应用的卡顿其实就是视觉上出现了不流畅的画面,引起了用户的注意,令其产生了一定的不适感。这也就意味着,在用户操作后,需要第一时间从视觉层面给与反馈响应,从而解决视觉动作带来的不适。
开发者可以在用户的交互动作开始时,从感知角度添加一些动画元素,比如单击效果、转场缩放、加载进度条、共享动画等,这些可以告诉用户目前状态发生了变化,APP在快速地运作着;而动画的背后是:数据的计算,布局的渲染,内容的加载等等,当新界面渲染显示完成,上述动画元素就可通过渐变消失、移出屏外等友好的方式退出视觉区域。
使用这样连贯的感知元素,能提供一种视觉隐喻,将用户的注意力从上个页面平滑地过渡到下个页面;交互动画表现得友好、有趣和实用,则用户在响应侧的体验会更加舒适,从主观上也会认为应用性能好、反应速度快。
场景案例
下面列举的示例通过多种不同的连贯动画,让应用使用者在操作过程中能够感受到程序的快速响应。
该示例场景:从留言箱的列表项点击后,执行响应切换进入个人页。在这一过程中,使用了三个动画组成其完整过程:
- 在整体界面的切换过程,使用了系统平台提供的转场动画,两个界面通过横向滑动,完成切换。
- 在转场动画的运行中,添加了头像移动缩放等变化的共享元素动画,以此体现具体响应元素的切换过渡。
- 在个人页列表加载前,添加了以列表轮廓为形态的骨架图闪烁动画,从而让用户感知新页面在加载运行的动态过程。
具体实现
用router+sharedTransition+animateTo的组合实现,具体操作思路如下:
- 分别在两个页面中设置pageTransition转场动画参数,然后在列表页头像的点击事件中添加router跳转,实现列表页到个人页的转场动画效果。
- 分别在两个页面的头像组件Image中添加属性sharedTransition,赋值相同的id进行唯一匹配,并添加共享元素动画的时间等相关参数,实现在转场中,头像从列表页向个人页移动的动画效果。
- 根据个人页内容的版面样式,实现一个骨架图组件,并使用animateTo对其添加一个反复渐显渐隐的动画行为,在个人页组件onAppear时呈现出来,然后在具体内容刷新出来后隐藏该动画元素。
关键代码
转场动画设置:
// page A 转场动画设置 pageTransition() { PageTransitionEnter({ type: RouteType.None, duration: 400 }) .slide(SlideEffect.Left) PageTransitionExit({ type: RouteType.None, duration: 400 }) .slide(SlideEffect.Left) }
列表页中共享元素动画设置:
// 列表中用头像做为共享元素,指定id为sharedImage+this.itemData.id Image(this.itemData.avatar) .height('40vp') .width('40vp') .borderRadius(8) .sharedTransition('sharedImage' + this.itemData.id, { duration: 500, curve: Curve.FastOutSlowIn, delay: 0 })
个人页中共享元素动画设置:
// 个人页共享元素,需要与上一页面id相同 Image(this.itemData.avatar) .size({ width: $r('app.float.user_image_size'), height: $r('app.float.user_image_size') })// .borderRadius($r('app.float.user_image_border_radius')) .borderRadius(8) .margin({ bottom: $r('app.float.user_image_padding'), top: $r('app.float.user_image_padding') }) .sharedTransition('sharedImage' + this.itemData.id, { duration: 500, curve: Curve.FastOutSlowIn, delay: 0 })
骨架图实现:
// 骨架图,通过渐显渐隐的方式呈现骨架动画 startAnimation(): void { animateTo(CommonConstants.SKELETON_ANIMATION, () => { this.listOpacity = CommonConstants.HALF_OPACITY; }); } // 骨架图布局 build() { Row() { List({ space: Constants.RESOURCE_LIST_SPACE }) { ForEach(SkeletonData, (item: SkeType) => { ListItem() { ArticleSkeletonView({ isMine: item.isMine, isFeed: item.isFeed }) } }) } .padding({ left: '12vp', right: '12vp' }) .lanes(1) .layoutWeight(1) .scrollBar(BarState.Off) Row() .layoutWeight(0) .backgroundColor($r('app.color.skeleton_color_medium')) } .width('100%') .opacity(this.listOpacity) .onAppear(() => { this.startAnimation(); }) }
总结
本文主要介绍了点击响应优化的相关内容,包括了优化的意义、优化的工具和各种方法等,通过实际案例分析了响应优化的实施过程和具体实测效果。优化涉及的类别和细节会很多,但核心宗旨都是用尽量少的性能资源去完成相关的需求功能。
当开发者遇到应用响应不理想的情况时,首先要考虑症结点所在,借助IDE提供的相关工具,分析性能瓶颈,然后针对性地逐一处理。
基于文中的优化方法,在优化处理原则上,一般遵循以下条例:
- 充分复用,减少UI绘制任务,移除冗余
- 延迟处理,延后处理不紧要任务
- 使用并发,充分利用CPU多核性能
- 利用缓存,采用合适的数据结构,合理的逻辑顺序
- 善用动画反馈,增强用户直观感受
当然,作为一个综合性的工作,优化往往不只局限于一两点,开发者需要从多个角度数管齐下,穷尽各种办法,反复测试统计,以得到预期的理想效果。另一方面,应用功能往往更新快、迭代多,上一版本的优化,可能在新功能的实现后,产生响应速度上的劣化,这就意味着开发者需要保持关键数据的跟踪指标,需要将响应速度的优化贯穿到应用生命的全部阶段,以提升使用者的操作体验,增强产品的留存和粘性,在发展迅猛的市场中把握技术带来的机遇。