1# 冗余刷新类问题解决方案 2 3## 概述 4不可见场景主要分为两类:前一个页面组件未销毁、当前页面部分部件未在显示区域内。冗余刷新是不可见组件的动画未停止导致。 5 6前一个页面未销毁:常出现在Router、Navigation等页面跳转,Tabs、Swiper等组件页面切换等场景。这些组件为了实现性能的高效加载和渲染在设计规格上不会主动销毁前一个页面。 7 8当前组件未在屏幕:常出现在Scroll等类似组件页面元素超出显示屏幕,LazyForEach预加载、组件visibility属性设置Visibility.None等场景。这类场景同样是基于高性能的考虑,允许屏幕外组件预加载。 9 10## 定位方法 11 12### 使用Trace工具 131. 使用[DevEco Profiler工具](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-optimization-overview#section2012922312284)抓取场景Trace,如果即使在静止状态,Trace仍然每一帧都相应Vsync,则有可能出现不可见刷新问题。 14 15 162. 通过Trace中的tag信息可以进一步确定标脏的组件ID。 17 18 193. 如果有明确的组件ID则可以直接使用inspector工具确定异常组件,如果组件ID是-1则大概率是空跑问题,可通过场景二分法确定。 20### 使用inspector工具 21 22通过使用Trace分析法获取组件ID后可以通过inspector工具或者抓取组件树的方法进一步确定异常组件的位置。 23### 场景二分定位法 24如果组件不在组件树上,需要复现场景,抓取操作路径上的每个步骤的Trace二分查找,找到异常组件未下树的Trace重复上述两个方法。 25## 解决思路 26 27### 接入可见接口法 28下方展示了使用[ImageAnimator](../reference/apis-arkui/arkui-ts/ts-basic-components-imageanimator.md)实现的动画组件,通过设置duration实现多个Pixelmap的循环播放。例如,当组件放置在Scroll容器中时,为避免组件划出屏幕导致的不可见空跑问题,可以通过监听组件移出屏幕的事件,修改动画播放状态,从而控制空跑。以下提供了几种接入可见性接口的实现方式,开发者可根据需要选择一种: 29 30[onVisibleAreaChange](../reference/apis-arkui/arkui-ts/ts-universal-component-visible-area-change-event.md#onvisibleareachange):可直接绑定到组件,当组件可见时每帧进行一次可见性计算,达到阈值时触发回调。 31[setOnVisibleAreaApproximateChange](../reference/apis-arkui/arkui-ts/ts-uicommonevent.md#setonvisibleareaapproximatechange)是onVisibleAreaChange()的低频优化版本,可以通过参数设置可见性计算的周期。例如,可以将expectedUpdateInterval设置为500ms。 32由于onVisibleAreaChange()在可见时会每帧进行一次计算检测,当组件数量较多、节点层次较深且帧率较高时,使用setOnVisibleAreaApproximateChange()可以减少计算负载,从而显著提升性能和降低功耗。 33 34```ts 35@Component 36struct ImageAnimatorTest { 37 private uid: number = -1; 38 private index: number = 0; 39 @State running: boolean = false; 40 @State animState: AnimationStatus = AnimationStatus.Initial; 41 42 // Method 1: use aboutToAppear to register a setOnVisibleAreaApproximateChange 43 aboutToAppear(): void { 44 this.uid = this.getUniqueId(); 45 hilog.info(0x0000, 'testTag', `getUniqueId in ImageAnimatorTest aboutAppear is ${this.uid}`); 46 let node = this.getUIContext().getFrameNodeByUniqueId(this.uid); 47 node?.commonEvent.setOnVisibleAreaApproximateChange( 48 { ratios: [0], expectedUpdateInterval: 500 }, 49 (isVisible, currentRatio) => { 50 hilog.info(0x0000, 'testTag', 51 `Method aboutToAppear: setOnVisibleAreaApproximateChange isVisible:${isVisible}, currentRatio:${currentRatio}`); 52 this.running = isVisible; 53 }) 54 } 55 56 build() { 57 Column() { 58 ImageAnimator() 59 .images([ 60 { src: $r('app.media.background') }, 61 { src: $r('app.media.foreground') } 62 ]) 63 .id(`ImageAnimator${this.index}}`) 64 .width('100%') 65 .height('30%') 66 .duration(3000) 67 .fillMode(FillMode.None) 68 .iterations(-1) 69 .state(this.running ? AnimationStatus.Running : 70 AnimationStatus.Paused)// Method 2: Directly use onVisibleAreaChange 71 .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean, currentRatio: number) => { 72 hilog.info(0x0000, 'testTag', 73 `Method Direct: onVisibleAreaChange isVisible:${isVisible}, currentRatio:${currentRatio}`); 74 if (isVisible && currentRatio >= 1.0) { 75 this.running = true; 76 } 77 if (!isVisible && currentRatio <= 0.0) { 78 this.running = false; 79 } 80 })// Method 3: use onAppear to register a setOnVisibleAreaApproximateChange 81 .onAppear(() => { 82 let node = this.getUIContext().getFrameNodeById(`ImageAnimator${this.index}`); 83 node?.commonEvent.setOnVisibleAreaApproximateChange( 84 { ratios: [0], expectedUpdateInterval: 500 }, 85 (isVisible, currentRatio) => { 86 this.running = isVisible; 87 hilog.info(0x0000, 'testTag', 88 `Method onAppear: setOnVisibleAreaApproximateChange isVisible:${isVisible}, currentRatio:${currentRatio}`); 89 } 90 ) 91 }) 92 } 93 } 94} 95``` 96### 组件条件卸载下树法 97利用if语句下树销毁的特性,通过状态变量控制组件的下树来达到停止动画的效果。 98> **说明:** 99> 100> [ohos_apng](https://gitee.com/openharmony-sig/ohos_apng)是以开源库[apng-js](https://github.com/davidmz/apng-js)为参考,基于1.1.2版本,通过重构解码算法,拆分出apng里各个帧图层的数据;使用arkts能力,将每一帧数据组合成imagebitmap,使用定时器调用每一帧数据,通过canvas渲染,从而达到帧动画效果。 101 102```ts 103import { apng, ApngController } from '@ohos/apng'; //开发者自行导入apng依赖库,详见上述说明。 104 105@Entry 106@Component 107struct RefreshExample { 108 @State isShow: boolean = false; 109 controller: ApngController = new ApngController(); 110 111 build() { 112 Column() { 113 Button('change') 114 .onClick(() => { 115 this.isShow = !this.isShow 116 } 117 ) 118 if (this.isShow) { 119 apng({ 120 src: $r('app.media.stack'), 121 controller: this.controller 122 }).margin({ top: 40 }) 123 } 124 } 125 } 126} 127``` 128 129### 状态变量监听法 130列表组件下拉刷新时,管理刷新动画的不可见现象。使用Canvas实现的[ohos_apng组件](https://gitee.com/openharmony-sig/ohos_apng)置于Refresh组件中,默认隐藏。监听Refresh组件的多种状态,通过onStateChange()方法监听RefreshStatus值。当Refresh组件处于收起状态(RefreshStatus为0和4)时,控制apngcontroller停止播放动画;当RefreshStatus处于拉起、回弹等状态(RefreshStatus为1、2和3)时,播放动画。 131 132```ts 133// VisibleComponent/entry/src/main/ets/pages/Index.ets 134import { apng, ApngController } from '@ohos/apng'; //开发者自行导入apng依赖库,详见上述说明。 135import { hilog } from '@kit.PerformanceAnalysisKit'; 136 137@Entry 138@Component 139struct RefreshExample { 140 @State isRefreshing: boolean = false; 141 @State isRunning: boolean = false; 142 @State arr: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; 143 controller: ApngController = new ApngController(); 144 145 @Builder 146 customRefreshComponent() { 147 Stack() { 148 Row() { 149 Column() { 150 apng({ 151 src: $r('app.media.stack'), 152 controller: this.controller 153 }) 154 .margin({ top: 40 }) 155 } 156 } 157 .alignItems(VerticalAlign.Center) 158 } 159 .align(Alignment.Center) 160 .clip(true) 161 .constraintSize({ minHeight: 32 }) 162 .width('100%') 163 } 164 165 build() { 166 Column() { 167 Refresh({ refreshing: $$this.isRefreshing, builder: this.customRefreshComponent() }) { 168 Scroll() { 169 Column() { 170 ImageAnimatorTest() 171 ForEach(this.arr, (item: string) => { 172 ListItem() { 173 Text('' + item) 174 .height(80) 175 .fontSize(16) 176 .textAlign(TextAlign.Center) 177 .fontColor(0xF1F3F5) 178 } 179 }, (item: string) => item) 180 } 181 } 182 .scrollBar(BarState.Off) 183 } 184 .backgroundColor(0xF1F3F5) 185 .pullToRefresh(true) 186 .refreshOffset(64) 187 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 188 // Use onStateChange and apngcontroller to control play and stop 189 .onStateChange((refreshStatus: RefreshStatus) => { 190 if (refreshStatus >= 1 && refreshStatus < 4) { 191 this.controller.play(); 192 } else { 193 this.controller.stop(); 194 } 195 hilog.info(0x0000, 'testTag', 'Refresh onStatueChange state is ' + refreshStatus); 196 }) 197 .onRefreshing(() => { 198 setTimeout(() => { 199 this.isRefreshing = false; 200 }, 2000) 201 hilog.info(0x0000, 'testTag', 'onRefreshing test') 202 }) 203 } 204 } 205} 206``` 207同理对navigation可以监听onHidden、onShow等事件,对tab可以监听onChange方法停止非当前index页面的动画。 208 209### 合理使用自定义动画 210不合理的使用animator、dispalysync也会导致冗余刷新问题。主要表现在三方使用Canvas自绘制动画没有在适当时机停止、使用animator开始动画未停止、注册了displaysync未在合适的时机解注册等。 211另外,不合理的delay设置也会导致不可见刷新,如开发者使用animator等函数设置延迟3s的动画,同时间隔3.5s再次调用这个动画等。 212## 系统组件默认策略 213系统组件一般用可见法解决,通过接入OnVisiableAreaChange回调在组件不可见时停止动画。当前各个组件适配如下表: 214 215|组件名称|设计动画项|不可见不刷新|是否有启停接口| 216| -------- | -------- | -------- | -------- | 217|[Image](../reference/apis-arkui/arkui-ts/ts-basic-components-image.md)|Gif、动图动画|已适配|Image不开放,DrawableDescriptor开放| 218|[ImageAnimator](../reference/apis-arkui/arkui-ts/ts-basic-components-imageanimator.md)|动画跳帧|未适配|有,参考官方文档| 219|[Text](https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-References/clouddb-text-0000001491435996)|跑马灯动画|已适配|overflow模式有启停方式| 220|[Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md)|自动轮播动画|已适配|-| 221|[LoadingProgress](../reference/apis-arkui/arkui-ts/ts-basic-components-loadingprogress.md)|播放动画|已适配|enableLoading属性可以启停动画| 222|[Marquee](../reference/apis-arkui/arkui-ts//ts-basic-components-marquee.md)|跑马灯动画|已适配|用户设置轮播次数| 223|[Progress](../reference/apis-arkui/arkui-ts//ts-basic-components-progress.md)|流光动画|已适配|status等属性可以控制动画启停| 224|高级组件|当前无自动播放动画|-|-| 225 226> **限制:** 227> 组件通过适配onVisiableAreaChange来实现不可见动画停止,受限于当前接口规格,如下场景无法覆盖: 228> - 被兄弟节点覆盖无法通知。 229> - stack堆叠不通知,Z轴遮挡不通知。 230> - PC多窗场景不通知。