應(yīng)用對用戶的輸入需要快速反饋峦睡,以提升交互體驗(yàn),因此本文提供了以下方法來提升應(yīng)用響應(yīng)速度青灼。
- 避免主線程被非UI任務(wù)阻塞
- 減少組件刷新的數(shù)量
避免主線程被非UI任務(wù)阻塞
在應(yīng)用響應(yīng)用戶輸入期間卷仑,應(yīng)用主線程應(yīng)盡可能只執(zhí)行UI任務(wù)(待顯示數(shù)據(jù)的準(zhǔn)備孽尽、可見視圖組件的更新等)于游,非UI的耗時(shí)任務(wù)(長時(shí)間加載的內(nèi)容等)建議通過異步任務(wù)延遲處理或者分配到其他線程處理毁葱。
使用組件異步加載特性
當(dāng)前系統(tǒng)提供的Image組件默認(rèn)生效異步加載特性,當(dāng)應(yīng)用在頁面上展示一批本地圖片的時(shí)候贰剥,會先顯示空白占位塊倾剿,當(dāng)圖片在其他線程加載完畢后,再替換占位塊蚌成。這樣圖片加載就可以不阻塞頁面的顯示前痘,給用戶帶來良好的交互體驗(yàn)。因此担忧,只在加載圖片耗時(shí)比較短的情況下建議下述代碼芹缔。
@Entry
@Component
struct ImageExample1 {
build() {
Column() {
Row() {
Image('resources/base/media/sss001.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
Image('resources/base/media/sss002.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
Image('resources/base/media/sss003.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
Image('resources/base/media/sss004.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
}
// 此處省略若干個(gè)Row容器,每個(gè)容器內(nèi)都包含如上的若干Image組件
}
}
}
建議:在加載圖片的耗時(shí)比較短的時(shí)候涵妥,通過異步加載的效果會大打折扣乖菱,建議配置Image的syncLoad屬性坡锡。
@Entry
@Component
struct ImageExample2 {
build() {
Column() {
Row() {
Image('resources/base/media/sss001.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
Image('resources/base/media/sss002.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
Image('resources/base/media/sss003.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
Image('resources/base/media/sss004.jpg')
.border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
}
// 此處省略若干個(gè)Row容器蓬网,每個(gè)容器內(nèi)都包含如上的若干Image組件
}
}
}
使用TaskPool線程池異步處理
當(dāng)前系統(tǒng)提供了TaskPool線程池,相比worker線程鹉勒,TaskPool提供了任務(wù)優(yōu)先級設(shè)置帆锋、線程池自動(dòng)管理機(jī)制,示例如下:
import taskpool from '@ohos.taskpool';
@Concurrent
function computeTask(arr: string[]): string[] {
// 模擬一個(gè)計(jì)算密集型任務(wù)
let count = 0;
while (count < 100000000) {
count++;
}
return arr.reverse();
}
@Entry
@Component
struct AspectRatioExample3 {
@State children: string[] = ['1', '2', '3', '4', '5', '6'];
aboutToAppear() {
this.computeTaskInTaskPool();
}
async computeTaskInTaskPool() {
const param = this.children.slice();
let task = new taskpool.Task(computeTask, param);
await taskpool.execute(task);
}
build() {
// 組件布局
}
}
創(chuàng)建異步任務(wù)
以下代碼展示了將一個(gè)長時(shí)間執(zhí)行的非UI任務(wù)通過Promise聲明成異步任務(wù)禽额,主線程可以先進(jìn)行用戶反饋-繪制初始頁面锯厢。等主線程空閑時(shí)皮官,再執(zhí)行異步任務(wù)。等到異步任務(wù)運(yùn)行完畢后实辑,重繪相關(guān)組件刷新頁面捺氢。
@Entry
@Component
struct AspectRatioExample4 {
@State private children: string[] = ['1', '2', '3', '4', '5', '6'];
private count: number = 0;
aboutToAppear() {
this.computeTaskAsync(); // 調(diào)用異步運(yùn)算函數(shù)
}
// 模擬一個(gè)計(jì)算密集型任務(wù)
computeTask() {
this.count = 0;
while (this.count < 100000000) {
this.count++;
}
this.children = this.children.reverse();
}
computeTaskAsync() {
setTimeout(() => { // 這里使用setTimeout來實(shí)現(xiàn)異步延遲運(yùn)行
this.computeTask();
}, 1000)
}
build() {
// 組件布局
}
}
減少刷新的組件數(shù)量
應(yīng)用刷新頁面時(shí)需要盡可能減少刷新的組件數(shù)量,如果數(shù)量過多會導(dǎo)致主線程執(zhí)行測量剪撬、布局的耗時(shí)過長摄乒,還會在自定義組件新建和銷毀過程中,多次調(diào)用aboutToAppear()残黑、aboutToDisappear()方法馍佑,增加主線程負(fù)載。
使用容器限制刷新范圍
反例:如果容器內(nèi)有組件被if條件包含梨水,if條件結(jié)果變更會觸發(fā)創(chuàng)建和銷毀該組件拭荤,如果此時(shí)影響到容器的布局,該容器內(nèi)所有組件都會刷新疫诽,導(dǎo)致主線程UI刷新耗時(shí)過長舅世。
以下代碼的Text('New Page')組件被狀態(tài)變量isVisible控制,isVisible為true時(shí)創(chuàng)建奇徒,false時(shí)銷毀歇终。當(dāng)isVisible發(fā)生變化時(shí),Stack容器內(nèi)的所有組件都會刷新:
@Entry
@Component
struct StackExample5 {
@State isVisible : boolean = false;
build() {
Column() {
Stack({alignContent: Alignment.Top}) {
Text().width('100%').height('70%').backgroundColor(0xd2cab3)
.align(Alignment.Center).textAlign(TextAlign.Center);
// 此處省略100個(gè)相同的背景Text組件
if (this.isVisible) {
Text('New Page').height("100%").height("70%").backgroundColor(0xd2cab3)
.align(Alignment.Center).textAlign(TextAlign.Center);
}
}
Button("press").onClick(() => {
this.isVisible = !(this.isVisible);
})
}
}
}
建議:對于這種受狀態(tài)變量控制的組件逼龟,在if外套一層容器评凝,減少刷新范圍。
@Entry
@Component
struct StackExample6 {
@State isVisible : boolean = false;
build() {
Column() {
Stack({alignContent: Alignment.Top}) {
Text().width('100%').height('70%').backgroundColor(0xd2cab3)
.align(Alignment.Center).textAlign(TextAlign.Center);
// 此處省略100個(gè)相同的背景Text組件
Stack() {
if (this.isVisible) {
Text('New Page').height("100%").height("70%").backgroundColor(0xd2cab3)
.align(Alignment.Center).textAlign(TextAlign.Center);
}
}.width('100%').height('70%')
}
Button("press").onClick(() => {
this.isVisible = !(this.isVisible);
})
}
}
}
按需加載列表組件的元素
反例:this.arr中的每一項(xiàng)元素都被初始化和加載腺律,數(shù)組中的元素有10000個(gè)奕短,主線程執(zhí)行耗時(shí)長。
@Entry
@Component
struct MyComponent7 {
@State arr: number[] = Array.from(Array<number>(10000), (v,k) =>k);
build() {
List() {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(`item value: ${item}`)
}
}, (item: number) => item.toString())
}
}
}
建議:這種情況下用LazyForEach替換ForEach匀钧,LazyForEach一般只加載可見的元素翎碑,避免一次性初始化和加載所有元素。
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): string {
return ''
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}
class MyDataSource extends BasicDataSource {
private dataArray: string[] = Array.from(Array<number>(10000), (v, k) => k.toString());
public totalCount(): number {
return this.dataArray.length
}
public getData(index: number): string {
return this.dataArray[index]
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
public pushData(data: string): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()
build() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
Text(item).fontSize(20).margin({ left: 10 })
}
}, (item:string) => item)
}
}
}
合理使用緩存提升響應(yīng)速度
緩存可以存儲經(jīng)常訪問的數(shù)據(jù)或資源之斯,當(dāng)下次需要訪問相同數(shù)據(jù)時(shí)日杈,可以直接從緩存中獲取,避免了重復(fù)的計(jì)算或請求佑刷,從而加快了響應(yīng)速度莉擒。
使用AVPlayer實(shí)例緩存提升視頻加載速度
AVPlayer實(shí)例的創(chuàng)建與銷毀都很消耗性能,針對這個(gè)問題可以使用實(shí)例緩存進(jìn)行優(yōu)化瘫絮,首次加載頁面時(shí)創(chuàng)建兩個(gè)實(shí)例涨冀,在打開新頁面時(shí)切換空閑實(shí)例,通過reset方法重置實(shí)例到初始化狀態(tài)麦萤。優(yōu)化點(diǎn)在于不需要頻繁創(chuàng)建銷毀實(shí)例鹿鳖,且reset方法性能優(yōu)于release方法扁眯。下面以AVPlayer為例列出正反例對比供參考。
反例:打開新頁面時(shí)創(chuàng)建實(shí)例翅帜,離開頁面時(shí)使用release方法銷毀實(shí)例姻檀。
import media from '@ohos.multimedia.media';
@Entry
@Component
struct Index {
private avPlayer: media.AVPlayer | undefined = undefined;
aboutToAppear(): void {
// 頁面創(chuàng)建時(shí)初始化AVPlayer實(shí)例
media.createAVPlayer().then((ret) => {
this.avPlayer = ret;
});
}
aboutToDisappear(): void {
// 離開頁面時(shí)銷毀AVPlayer實(shí)例
if (this.avPlayer) {
this.avPlayer.release();
}
this.avPlayer = undefined;
}
build() {
// 組件布局
}
}
正例:首次加載頁面時(shí)維護(hù)兩個(gè)實(shí)例,在切換頁面時(shí)切換實(shí)例涝滴,并將之前的實(shí)例通過reset方法重置施敢。
import media from '@ohos.multimedia.media';
@Entry
@Component
struct Index {
private avPlayer: media.AVPlayer | undefined = undefined;
private avPlayerManager: AVPlayerManager = AVPlayerManager.getInstance();
aboutToAppear(): void {
this.avPlayerManager.switchPlayer();
this.avPlayer = this.avPlayerManager.getCurrentPlayer();
}
aboutToDisappear(): void {
this.avPlayerManager.resetCurrentPlayer();
this.avPlayer = undefined;
}
build() {
// 組件布局
}
}
class AVPlayerManager {
private static instance?: AVPlayerManager;
private player1?: media.AVPlayer;
private player2?: media.AVPlayer;
private currentPlayer?: media.AVPlayer;
public static getInstance(): AVPlayerManager {
if (!AVPlayerManager.instance) {
AVPlayerManager.instance = new AVPlayerManager();
}
return AVPlayerManager.instance;
}
async AVPlayerManager() {
this.player1 = await media.createAVPlayer();
this.player2 = await media.createAVPlayer();
}
/**
* 切換頁面時(shí)切換AVPlayer實(shí)例
*/
switchPlayer(): void {
if (this.currentPlayer === this.player1) {
this.currentPlayer = this.player2;
} else {
this.currentPlayer = this.player1;
}
}
getCurrentPlayer(): media.AVPlayer | undefined {
return this.currentPlayer;
}
/**
* 使用reset方法重置AVPlayer實(shí)例
*/
resetCurrentPlayer(): void {
this.currentPlayer?.pause(() => {
this.currentPlayer?.reset();
});
}
}
合理使用預(yù)加載提升響應(yīng)速度
使用NodeContainer提前渲染降低響應(yīng)時(shí)延
應(yīng)用啟動(dòng)時(shí)有廣告頁的場景下。如果先渲染廣告頁而后再渲染首頁狭莱,很可能造成首頁響應(yīng)時(shí)延較長僵娃,影響用戶體驗(yàn)。針對此類問題可以使用NodeContainer在廣告頁渲染時(shí)同步渲染首頁腋妙,等到跳轉(zhuǎn)到首頁時(shí)直接送顯默怨,提高響應(yīng)速度。
反例:按次序依次渲染送顯
主要代碼邏輯如下:
1骤素、模擬廣告頁匙睹,通過點(diǎn)擊不同按鈕分別進(jìn)入普通頁面和預(yù)加載頁面
// Index.ets
import router from '@ohos.router';
@Entry
@Component
struct Index {
build() {
Column({ space: 5 }) {
// 進(jìn)入普通頁面
Button("普通頁面")
.type(ButtonType.Capsule)
.onClick(() => {
router.pushUrl({ url: 'pages/CommonPage' })
})
// 進(jìn)入預(yù)加載頁面
Button("預(yù)加載頁面")
.type(ButtonType.Capsule)
.onClick(() => {
router.pushUrl({ url: 'pages/PreloadedPage' })
})
}.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
}
}
2、普通首頁济竹,也即按順序普通渲染的頁面
// CommonPage.ets
import { MyBuilder, getNumbers } from '../builder/CustomerBuilder';
@Entry
@Component
struct CommonPage {
build() {
Row() {
MyBuilder(getNumbers())
}
}
}
3痕檬、自定義builder,用來定制頁面結(jié)構(gòu)
// CustomerBuilder.ets
@Builder
export function MyBuilder(numbers: string[]) {
Column() {
List({ space: 20, initialIndex: 0 }) {
ForEach(numbers, (item: string) => {
ListItem() {
Text('' + item)
.width('100%')
.height(50)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
}, (day: string) => day)
}
.listDirection(Axis.Vertical) // 排列方向
.scrollBar(BarState.Off)
.friction(0.6)
.divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) // 每行之間的分界線
.edgeEffect(EdgeEffect.Spring) // 邊緣效果設(shè)置為Spring
.width('90%')
.height('100%')
}
.width('100%')
.height('100%')
.backgroundColor(0xDCDCDC)
.padding({ top: 5 })
}
export const getNumbers = (): string[] => {
const numbers: string[] = [];
for (let i = 0; i < 100; i++) {
numbers.push('' + i)
}
return numbers;
}
正例:在啟動(dòng)時(shí)預(yù)加載首頁
主要代碼邏輯如下:
1送浊、應(yīng)用啟動(dòng)時(shí)提前創(chuàng)建首頁
// EntryAbility.ets
import { ControllerManager } from '../builder/CustomerController';
import { getNumbers } from '../builder/CustomerBuilder';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
window.getLastWindow(this.context, (err: BusinessError, data) => {
if (err.code) {
console.error('Failed to obtain top window. Cause:' + JSON.stringify(err));
return;
}
// 提前創(chuàng)建
ControllerManager.getInstance().createNode(data.getUIContext(), getNumbers());
})
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
// 清空組件梦谜,防止內(nèi)存泄漏
ControllerManager.getInstance().clearNode();
}
}
2、預(yù)加載的首頁袭景,使用NodeContainer進(jìn)行占位唁桩,當(dāng)跳轉(zhuǎn)到本頁時(shí)直接將提前創(chuàng)建完成的首頁填充
// PreloadedPage.ets
import { ControllerManager } from '../builder/CustomerController';
@Entry
@Component
struct PreloadedPage {
build() {
Row() {
NodeContainer(ControllerManager.getInstance().getNode())
}
}
}
3、自定義NodeController耸棒,并提供提前創(chuàng)建首頁的能力
// CustomerController.ets
import { UIContext } from '@ohos.arkui.UIContext';
import { NodeController, BuilderNode, FrameNode } from "@ohos.arkui.node";
import { MyBuilder } from './CustomerBuilder';
export class MyNodeController extends NodeController {
private rootNode: BuilderNode<[string[]]> | null = null;
private wrapBuilder: WrappedBuilder<[string[]]> = wrapBuilder(MyBuilder);
private numbers: string[] | null = null;
constructor(numbers: string[]) {
super();
this.numbers = numbers;
}
makeNode(uiContext: UIContext): FrameNode | null {
if (this.rootNode != null) {
// 返回FrameNode節(jié)點(diǎn)
return this.rootNode.getFrameNode();
}
// 返回null控制動(dòng)態(tài)組件脫離綁定節(jié)點(diǎn)
return null;
}
// 通過UIContext初始化BuilderNode荒澡,再通過BuilderNode中的build接口初始化@Builder中的內(nèi)容
initNode(uiContext: UIContext) {
if (this.rootNode != null) {
return;
}
// 創(chuàng)建節(jié)點(diǎn),需要uiContext
this.rootNode = new BuilderNode(uiContext)
// 創(chuàng)建組件
this.rootNode.build(this.wrapBuilder, this.numbers)
}
}
export class ControllerManager {
private static instance?: ControllerManager;
private myNodeController?: MyNodeController;
static getInstance(): ControllerManager {
if (!ControllerManager.instance) {
ControllerManager.instance = new ControllerManager();
}
return ControllerManager.instance;
}
/**
* 初始化需要UIContext 需在Ability獲取
* @param uiContext
* @param numbers
*/
createNode(uiContext: UIContext, numbers: string[]) {
// 創(chuàng)建NodeController
this.myNodeController = new MyNodeController(numbers);
this.myNodeController.initNode(uiContext);
}
/**
* 自定義獲取NodeController實(shí)例接口
* @returns MyNodeController
*/
getNode(): MyNodeController | undefined {
return this.myNodeController;
}
/**
* 解除占用与殃,防止內(nèi)存泄漏
*/
clearNode(): void {
this.myNodeController = undefined;
}
}
通過SmartPerf-Host工具抓取相關(guān)trace進(jìn)行分析首頁響應(yīng)時(shí)延单山,其中主要關(guān)注兩個(gè)trace tag分別是DispatchTouchEvent代表點(diǎn)擊事件和MarshRSTransactionData代表響應(yīng),如下圖所示:
反例響應(yīng)時(shí)延:18.1ms
正例響應(yīng)時(shí)延:9.4ms
由上述對比數(shù)據(jù)即可得出結(jié)論,預(yù)加載首頁能優(yōu)化首頁響應(yīng)時(shí)延幅疼。
寫在最后
如果你覺得這篇內(nèi)容對你還蠻有幫助米奸,我想邀請你幫我三個(gè)小忙:
- 點(diǎn)贊,轉(zhuǎn)發(fā)衣屏,有你們的 『點(diǎn)贊和評論』躏升,才是我創(chuàng)造的動(dòng)力辩棒。
- 關(guān)注小編狼忱,同時(shí)可以期待后續(xù)文章ing??膨疏,不定期分享原創(chuàng)知識。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識點(diǎn)钻弄,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu