1# 避免在主线程中执行耗时操作
2## 简介
3在应用开发中,经常会调用执行耗时的接口,比如服务端数据接口,本地文件读取接口。如果不进行合理的处理,可能会引起卡顿等性能问题。
4## 问题场景
5列表无限滑动的场景,在即将触底的时候需要进行数据请求,如果在主线程中直接处理请求数据,可能会导致滑动动画被中断。如果回调函数处理的耗时较长,会直接阻塞主线程,卡顿就会非常明显。使用异步执行的方式进行异步调用,回调函数的执行还是会在主线程,一样会阻塞UI绘制和渲染。场景预览如下,列表滑动过程中,图片会显示延迟。
6
7![](./figures/avoid_time_consuming_demo.gif)
8
9## 优化示例
10### 优化前代码示例
11如下代码实现了一个瀑布流,每一个元素都是一张图片,在滑动即将触底时调用异步函数mockRequestData获取新数据,并将数据写入数据源。异步函数mockRequestData用于模拟耗时的网络请求,从rawfile中读取数据,将数据处理后返回。
12```ts
13  build() {
14    Column({ space: 2 }) {
15      WaterFlow() {
16        LazyForEach(this.dataSource, (item: ModelDetailVO) => {
17          FlowItem() {
18            Column() {
19              Image(item.url)
20            }
21          }
22          .onAppear(() => {
23            // 即将触底时提前增加数据
24            if (item.id + 10 === this.dataSource.totalCount()) {
25              // 通过子线程获取数据,传入当前的数据长度,用于赋给数据的ID值
26              this.mockRequestData().then((data: ModelDetailVO[]) => {
27                for (let i = 0; i < data.length; i++) {
28                  this.dataSource.addLastItem(data[i]);
29                }
30              })
31            }
32          })
33        }, (item: string) => item)
34      }
35    }
36  }
37
38  async mockRequestData(): Promise<ModelDetailVO[]> {
39    let result: modelDetailDTO[] = [];
40    // data.json是存在本地的json数据,大小大约20M,模拟从网络端获取数据
41    await getContext().resourceManager.getRawFileContent("data.json").then((data: Uint8Array) => {
42      // 耗时回调函数
43      let jsonData = buffer.from(data).toString();
44      let res: responseData = JSON.parse(jsonData);
45      result = res.data;
46    })
47    return this.transArrayDTO2VO(result);
48  }
49  // ...
50```
51编译运行后,通过[SmartPerf Host](./performance-optimization-using-smartperf-host.md)工具抓取Trace。如下图所示,其中红色框选的部分就是getRawFileContent的回调耗时。
52
53![](./figures/trace_mainthread_callback.png)
54
55从图中可以看到,在主线程中出现了大块的耗时,直接导致用户在滑动的时候能感受到明显的卡顿。异步回调函数最后也由主线程执行,所以应该尽量避免在回调函数中执行耗时操作。
56
57### 优化代码
58
59#### 优化思路:使用多线程能力
60使用系统自带的[TaskPool](../arkts-utils//taskpool-introduction.md)多线程能力。
61```ts
62  build() {
63    Column({ space: 2 }) {
64      WaterFlow() {
65        LazyForEach(this.dataSource, (item: ModelDetailVO) => {
66          FlowItem() {
67            Column() {
68              Image(item.url)
69            }
70          }
71          .onAppear(() => {
72            // 即将触底时提前增加数据
73            if (item.id + 10 === this.dataSource.totalCount()) {
74              // 通过子线程获取数据,传入当前的数据长度,用于赋给数据的ID值
75              taskpoolExecute(this.dataSource.totalCount()).then((data: ModelDetailVO[]) => {
76                for (let i = 0; i < data.length; i++) {
77                  this.dataSource.addLastItem(data[i]);
78                }
79              })
80            }
81          })
82        }, (item: string) => item)
83      }
84    }
85  }
86
87  // 注意:以下方法和类声明均在组件外声明
88  async function taskpoolExecute(index: number): Promise<ModelDetailVO[]> {
89    // context需要手动传入子线程
90    let task: taskpool.Task = new taskpool.Task(mockRequestData, index, getContext());
91    return await taskpool.execute(task) as ModelDetailVO[];
92  }
93
94  // 标记并发执行函数
95  @Concurrent
96  async function mockRequestData(index: number, context: Context): Promise<ModelDetailVO[]> {
97    let result: modelDetailDTO[] = [];
98    // data.json是存在本地的json数据,大小大约20M,模拟从网络端获取数据
99    await context.resourceManager.getRawFileContent("data.json").then((data: Uint8Array) => {
100      let jsonData = buffer.from(data).toString();
101      let res: responseData = JSON.parse(jsonData);
102      result = res.data;
103    })
104    return transArrayDTO2VO(result, index);
105  }
106```
107
108在上面的代码里,优化的思路主要是用子线程处理耗时操作,避免在主线程中执行耗时操作影响UI渲染,编译运行后,通过[SmartPerf Host](./performance-optimization-using-smartperf-host.md)工具抓取Trace。如下图所示,原先在主线程中的getRawFileContent的标签转移到了TaskWorker线程。
109
110![](./figures//trace_taskpool_callback.png)
111
112从图中可以看到,主线程阻塞耗时明显减少,同时在右上角出现了新的trace,__H:Deserialize__,这个trace表示在反序列化taskpool线程返回的数据。依然存在一定耗时(17ms) 容易出现丢帧等问题。针对跨线程的序列化耗时问题,系统提供了[@Sendable装饰器](../arkts-utils/arkts-sendable.md#sendable装饰器)来实现内存共享。可以在返回的类对象ModelDetailVO上使用@Sendable装饰器,继续优化性能。
113
114#### 优化思路:可以使用@Sendable装饰器提升数据传输和同步效率
115多线程存在线程间通信耗时问题,如果涉及数据较大的情况,可以使用[@Sendable](../arkts-utils/arkts-sendable.md)。
116
117```c++
118  build() {
119    Column({ space: 2 }) {
120      WaterFlow({}) {
121        LazyForEach(this.dataSource, (item: ModelDetailVO) => {
122          FlowItem() {
123            Column() {
124              Image(item.url)
125            }
126          }
127          .onAppear(() => {
128            // 即将触底时提前增加数据
129            if (item.id + 10 === this.dataSource.totalCount()) {
130              // 通过子线程获取数据,传入当前的数据长度,用于赋给数据的ID值
131              taskpoolExecute(this.dataSource.totalCount()).then((data: ModelDetailVO[]) => {
132                for (let i = 0; i < data.length; i++) {
133                  this.dataSource.addLastItem(data[i]);
134                }
135              })
136            }
137          })
138        }, (item: string) => item)
139      }
140    }
141  }
142
143  // 注意:以下方法和类声明均在组件外声明
144  async function taskpoolExecute(index: number): Promise<ModelDetailVO[]> {
145    // context需要手动传入子线程
146    let task: taskpool.Task = new taskpool.Task(mockRequestData, index, getContext());
147    return await taskpool.execute(task) as ModelDetailVO[];
148  }
149
150  // 标记并发执行函数
151  @Concurrent
152  async function mockRequestData(index: number, context: Context): Promise<ModelDetailVO[]> {
153    let result: modelDetailDTO[] = [];
154    // data.json是存在本地的json数据,大小大约20M,模拟从网络端获取数据
155    await context.resourceManager.getRawFileContent("data.json").then((data: Uint8Array) => {
156      let jsonData = buffer.from(data).toString();
157      let res: responseData = JSON.parse(jsonData);
158      result = res.data;
159    })
160    return transArrayDTO2VO(result, index);
161  }
162
163  @Sendable
164  class ModelDetailVO {
165    id: number = 0;
166    name: string = "";
167    url: string = "";
168  }
169
170  // ...
171```
172上面的代码在子线程返回的类对象上使用了@Sendable,系统会使用共享内存的方式处理使用了@Sendable的类,从而降低反序列化的开销。
173
174![](./figures//trace_sendable_callback.png)
175
176从图中可以看到,反序列化的大小和耗时明显变少。
177
178## 总结
179通过上面的示例代码和优化过程,可以看到在主线程的回调函数中处理耗时操作会直接阻塞主线程,用户能感知到明显的卡顿,用子线程配合@Sendable可以有效的优化该场景的性能。
180