鴻蒙(HarmonyOS)性能優(yōu)化實(shí)戰(zhàn)-Swiper高性能開發(fā)

背景

在應(yīng)用開發(fā)中,Swiper 組件常用于翻頁(yè)場(chǎng)景乌企,比如:桌面阁最、圖庫(kù)等應(yīng)用戒祠。Swiper 組件滑動(dòng)切換頁(yè)面時(shí),基于按需加載原則通常會(huì)在下一個(gè)頁(yè)面將要顯示時(shí)才對(duì)該頁(yè)面進(jìn)行加載和布局繪制速种,這個(gè)過(guò)程包括:

  • 如果該頁(yè)面使用了@Component 裝飾的自定義組件姜盈,那么自定義組件的 build 函數(shù)會(huì)被執(zhí)行并創(chuàng)建內(nèi)部的 UI 組件;

  • 如果使用了LazyForEach配阵,會(huì)執(zhí)行 LazyForEach 的 UI 生成函數(shù)生成 UI 組件馏颂;

  • 在 UI 組件構(gòu)建完成后,會(huì)對(duì) UI 組件進(jìn)行布局測(cè)算和繪制棋傍。

針對(duì)復(fù)雜頁(yè)面場(chǎng)景救拉,該過(guò)程可能會(huì)持續(xù)較長(zhǎng)時(shí)間,導(dǎo)致滑動(dòng)過(guò)程中出現(xiàn)卡頓瘫拣,對(duì)滑動(dòng)體驗(yàn)造成負(fù)面影響亿絮,甚至成為整個(gè)應(yīng)用的性能瓶頸。如在圖庫(kù)大圖瀏覽場(chǎng)景中麸拄,若不使用預(yù)加載機(jī)制派昧,每次都將在滑動(dòng)開始的首幀去加載下一張圖片,會(huì)導(dǎo)致首幀耗時(shí)過(guò)長(zhǎng)甚至掉幀拢切,拖慢應(yīng)用性能斗锭。

為了解決上述問(wèn)題,可以使用 Swiper 組件的預(yù)加載機(jī)制失球,利用主線程的空閑時(shí)間來(lái)提前構(gòu)建和布局繪制組件,優(yōu)化滑動(dòng)體驗(yàn)实苞。

使用場(chǎng)景

如果開發(fā)者的應(yīng)用場(chǎng)景屬于加載較為耗時(shí)的場(chǎng)景時(shí)烈疚,尤其是下列場(chǎng)景,推薦使用 Swiper 預(yù)加載功能爷肝。

  • Swiper 的子組件大于等于五個(gè)猾浦;

  • Swiper 的子組件具有復(fù)雜的動(dòng)畫;

  • Swiper 的子組件加載時(shí)需要執(zhí)行網(wǎng)絡(luò)請(qǐng)求等耗時(shí)操作金赦;

  • Swiper 的子組件包含大量需要渲染的圖像或資源对嚼。

Swiper 預(yù)加載機(jī)制說(shuō)明

預(yù)加載機(jī)制是 Swiper 組件中一個(gè)重要的特性夹抗,允許 Swiper 滑動(dòng)到下一個(gè)子組件之前提前加載后續(xù)頁(yè)面的內(nèi)容,其主要目的是提高應(yīng)用滑動(dòng)時(shí)的流暢性和響應(yīng)速度纵竖。當(dāng)用戶嘗試滑動(dòng)到下一個(gè)子組件時(shí)漠烧,如果下一個(gè)子組件的內(nèi)容已經(jīng)提前加載完畢已脓,那么滑動(dòng)就會(huì)立即發(fā)生,否則 Swiper 組件需要在加載下一個(gè)子組件的同時(shí)處理滑動(dòng)事件度液,對(duì)滑動(dòng)體驗(yàn)造成負(fù)面影響恨诱。當(dāng)前 Swiper 組件的預(yù)加載在用戶滑動(dòng)離手動(dòng)效開始時(shí)觸發(fā)媳瞪,離手動(dòng)效的計(jì)算在渲染線程中進(jìn)行蛇受,因此主線程有空閑的時(shí)間可以進(jìn)行預(yù)加載的操作厕鹃。配合 LazyForEach 的按需加載和銷毀能力,可以在優(yōu)化滑動(dòng)體驗(yàn)基礎(chǔ)上節(jié)省內(nèi)存占用把将。

使用指導(dǎo)

  • 預(yù)加載子組件的個(gè)數(shù)在cachedCount屬性中配置忆矛。

Swiper 共 5 頁(yè),當(dāng)開發(fā)者設(shè)置了 cacheCount 屬性為 1 且 loop 屬性為 false 時(shí)洽议,預(yù)加載的結(jié)果如下:\


Swiper 共 5 頁(yè)漫拭,當(dāng)開發(fā)者設(shè)置了 cacheCount 屬性為 1 且 loop 屬性為 true 時(shí),預(yù)加載的結(jié)果如下:\

  • Swiper 組件的子組件使用LazyForEach動(dòng)態(tài)加載和銷毀組件审胚。

示例

class MyDataSource implements IDataSource { // LazyForEach的數(shù)據(jù)源
  private list: number[] = [];

  constructor(list: number[]) {
    this.list = list;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): number {
    return this.list[index];
  }

  registerDataChangeListener(_: DataChangeListener): void {
  }

  unregisterDataChangeListener(): void {
  }
}

@Component
struct SwiperChildPage { // Swiper的子組件
  @State arr: number[] = [];

  aboutToAppear(): void {
    for (let i = 1; i <= 100; i++) {
      this.arr.push(i);
    }
  }

  build() {
    Column() {
      List({ space: 20 }) {
        ForEach(this.arr, (index: number) => {
          ListItem() {
            Text(index.toString())
              .height('4.5%')
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .backgroundColor(0xFFFFFF)
          }
          .border({ width: 2, color: Color.Green })
        }, (index: number) => index.toString());
      }
      .height("95%")
      .width("95%")
      .border({ width: 3, color: Color.Red })
      .lanes({ minLength: 40, maxLength: 40 })
      .alignListItem(ListItemAlign.Start)
      .scrollBar(BarState.Off)

    }.width('100%').height('100%').padding({ top: 5 });
  }
}

@Entry
@Preview
@Component
struct SwiperExample {
  private dataSrc: MyDataSource = new MyDataSource([]);

  aboutToAppear(): void {
    let list: Array<number> = []
    for (let i = 1; i <= 10; i++) {
      list.push(i);
    }
    this.dataSrc = new MyDataSource(list);
  }

  build() {
    Column({ space: 5 }) {
      Swiper() {
        LazyForEach(this.dataSrc, (_: number) => {
          SwiperChildPage();
        }, (item: number) => item.toString());
      }
      .loop(false)
      .cachedCount(1) // 提前加載后一項(xiàng)的內(nèi)容
      .indicator(true)
      .duration(100)
      .displayArrow({
        showBackground: true,
        isSidebarMiddle: true,
        backgroundSize: 40,
        backgroundColor: Color.Orange,
        arrowSize: 25,
        arrowColor: Color.Black
      }, false)
      .curve(Curve.Linear)

    }.width('100%')
    .margin({ top: 5 })
  }
}

驗(yàn)證效果

為了更好地體現(xiàn) Swiper 預(yù)加載機(jī)制帶來(lái)的性能優(yōu)化效果,用例采用下列前置條件:

  • Swiper 的子組件為帶有 100 個(gè) ListItem 的 List 組件痘系;

  • Swiper 組件共有 10 個(gè) List 子組件。

在該場(chǎng)景下临谱,使用 Swiper 預(yù)加載機(jī)制可以為每個(gè)翻頁(yè)動(dòng)作節(jié)省約40%的時(shí)間,同時(shí)保證翻頁(yè)時(shí)不丟幀城豁,保證翻頁(yè)的流暢度抄课。

優(yōu)化建議

由于組件構(gòu)建和布局計(jì)算需要一定時(shí)間,cachedCount 的數(shù)量也不是設(shè)置得越大越好间聊,過(guò)大的 cachedCount 可能會(huì)導(dǎo)致應(yīng)用性能降低抵拘。當(dāng)前 Swiper 組件滑動(dòng)離手后的動(dòng)效時(shí)間大約是 400ms,如果應(yīng)用加載一個(gè)子組件的時(shí)間在 100ms~200ms 之間尚蝌,為了在離手動(dòng)效時(shí)間內(nèi)完成組件的預(yù)加載充尉,cachedCount 屬性建議設(shè)置為 1 或 2,設(shè)置過(guò)大會(huì)導(dǎo)致主線程阻塞而產(chǎn)生卡頓姿鸿。

那么方案可以繼續(xù)優(yōu)化倒源,Swiper 組件有一個(gè)OnAnimationStart回調(diào)接口相速,切換動(dòng)畫開始時(shí)觸發(fā)該回調(diào)鲜锚。此時(shí),主線程空閑旺隙,應(yīng)用可以充分利用這段時(shí)間進(jìn)行圖片等資源的預(yù)加載骏令,減少后續(xù) cachedCount 范圍內(nèi)的節(jié)點(diǎn)預(yù)加載耗時(shí)。

示例

Swiper 子組件頁(yè)面代碼如下:

在子組件首次構(gòu)建(生命周期執(zhí)行到aboutToAppear)時(shí)周拐,先判斷 dataSource 中該 index 的數(shù)據(jù)是否有數(shù)據(jù),若無(wú)數(shù)據(jù)則先進(jìn)行資源加載审丘,再構(gòu)建節(jié)點(diǎn)勾给。若有數(shù)據(jù),則直接構(gòu)建節(jié)點(diǎn)即可脓钾。

import image from '@ohos.multimedia.image';
import { MyDataSource } from './Index'

@Component
export struct PhotoItem { //Swiper的子組件
  myIndex: number = 0;
  private dataSource: MyDataSource = new MyDataSource([]);
  context = getContext(this);
  @State imageContent: image.PixelMap | undefined = undefined;

  aboutToAppear(): void {
    console.info(`aboutToAppear` + this.myIndex);
    this.imageContent = this.dataSource.getData(this.myIndex)?.image;
    if (!this.imageContent) { // 先判斷dataSource中該index的數(shù)據(jù)是否有數(shù)據(jù)桩警,若無(wú)數(shù)據(jù)則先進(jìn)行資源加載
      try {
        // 獲取resourceManager資源管理器
        const resourceMgr = this.context.resourceManager;
        // 獲取rawfile文件夾下item.jpg的ArrayBuffer
        let str = "item" + (this.myIndex + 1) + ".jpg";
        resourceMgr.getRawFileContent(str).then((value) => {
          // 創(chuàng)建imageSource
          const imageSource = image.createImageSource(value.buffer);
          imageSource.createPixelMap().then((value) => {
            console.log("aboutToAppear push" + this.myIndex)
            this.dataSource.addData(this.myIndex, { description: "" + this.myIndex, image: value })
            this.imageContent = value;
          })
        })
      } catch (err) {
        console.log("error code" + err);
      }
    }
  }

  build() {
    Column() {
      Image(this.imageContent)
        .width("100%")
        .height("100%")
    }
  }
}

Swiper 主頁(yè)面的代碼如下:

import Curves from '@ohos.curves';
import { PhotoItem } from './PhotoItem'
import image from '@ohos.multimedia.image';

interface MyObject {
  description: string,
  image: image.PixelMap,
};

export class MyDataSource implements IDataSource {
  private list: MyObject[] = []

  constructor(list: MyObject[]) {
    this.list = list
  }

  totalCount(): number {
    return this.list.length
  }

  getData(index: number): MyObject {
    return this.list[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
  }

  addData(index: number, data: MyObject) {
    this.list[index] = data;
  }
}

@Entry
@Component
struct Index {
  @State currentIndex: number = 0;
  cacheCount: number = 1
  swiperController: SwiperController = new SwiperController();
  private data: MyDataSource = new MyDataSource([]);
  context = getContext(this);

  aboutToAppear() {
    let list: MyObject[] = []
    for (let i = 0; i < 6; i++) {
      list.push({ description: "", image: this.data.getData(this.currentIndex)?.image })
    }
    this.data = new MyDataSource(list)
  }

  build() {
    Swiper(this.swiperController) {
      LazyForEach(this.data, (item: MyObject, index?: number) => {
        PhotoItem({
          myIndex: index,
          dataSource: this.data
        })
      })
    }
    .cachedCount(this.cacheCount)
    .curve(Curves.interpolatingSpring(0, 1, 228, 30))
    .index(this.currentIndex)
    .indicator(true)
    .loop(false)
    // 在OnAnimationStart接口回調(diào)中進(jìn)行預(yù)加載資源的操作
    .onAnimationStart((index: number, targetIndex: number) => {
      console.info("onAnimationStart " + index + " " + targetIndex);
      if (targetIndex !== index) {
        try {
          // 獲取resourceManager資源管理器
          const resourceMgr = this.context.resourceManager;
          // 獲取rawfile文件夾下item.jpg的ArrayBuffer
          let str = "item" + (targetIndex + this.cacheCount + 2) + ".jpg";
          resourceMgr.getRawFileContent(str).then((value) => {
            // 創(chuàng)建imageSource
            const imageSource = image.createImageSource(value.buffer);
            imageSource.createPixelMap().then((value) => {
              this.data.addData(targetIndex + this.cacheCount + 1, {
                description: "" + (targetIndex + this.cacheCount + 1),
                image: value
              })
            })
          })
        } catch (err) {
          console.log("error code" + err);
        }
      }
    })
    .width('100%')
    .height('100%')
  }
}

總結(jié)

  • Swiper 組件的預(yù)加載機(jī)制與 LazyForEach 結(jié)合使用,能夠達(dá)到最佳優(yōu)化效果柱蟀。

  • 預(yù)加載的 cachedCount 并非越大越好,需要結(jié)合單個(gè)子組件加載耗時(shí)來(lái)設(shè)置畜眨。假設(shè)一個(gè)子組件的加載耗時(shí)為 Nms术瓮,那么 cachedCount 推薦設(shè)置為小于 400/N。

  • 如果應(yīng)用有非常高的性能優(yōu)化需求恬汁,Swiper 預(yù)加載機(jī)制可搭配 OnAnimationStart 接口回調(diào)使用辜伟,進(jìn)一步提升預(yù)加載的效率。

寫在最后

如果你覺得這篇內(nèi)容對(duì)你還蠻有幫助约巷,我想邀請(qǐng)你幫我三個(gè)小忙

  • 點(diǎn)贊旱捧,轉(zhuǎn)發(fā)踩麦,有你們的 『點(diǎn)贊和評(píng)論』谓谦,才是我創(chuàng)造的動(dòng)力顽铸。
  • 關(guān)注小編,同時(shí)可以期待后續(xù)文章ing??星压,不定期分享原創(chuàng)知識(shí)鬼譬。
  • 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識(shí)點(diǎn),請(qǐng)移步前往小編:https://gitee.com/MNxiaona/733GH/blob/master/jianshu
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末竣贪,一起剝皮案震驚了整個(gè)濱河市巩螃,隨后出現(xiàn)的幾起案子避乏,更是在濱河造成了極大的恐慌,老刑警劉巖拍皮,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铆帽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡萨螺,警方通過(guò)查閱死者的電腦和手機(jī)愧驱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惹盼,“玉大人惫确,你說(shuō)我怎么就攤上這事⊙诟颍” “怎么了陈肛?”我有些...
    開封第一講書人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵句旱,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我谈撒,道長(zhǎng)啃匿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任夹厌,我火速辦了婚禮裆悄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘崖技。我一直安慰自己钟哥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開白布吁恍。 她就那樣靜靜地躺著播演,像睡著了一般。 火紅的嫁衣襯著肌膚如雪翼闽。 梳的紋絲不亂的頭發(fā)上洲炊,一...
    開封第一講書人閱讀 51,245評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音询微,去河邊找鬼。 笑死书聚,一個(gè)胖子當(dāng)著我的面吹牛藻雌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播西雀,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼歉摧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼叁温!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起膝但,我...
    開封第一講書人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤跟束,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后灭贷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體略贮,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了揽祥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡府树,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情站绪,我是刑警寧澤恢准,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站涂召,受9級(jí)特大地震影響敏沉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盟迟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一攒菠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卓起,春花似錦凹炸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至笆怠,卻和暖如春誊爹,著一層夾襖步出監(jiān)牢的瞬間瓢捉,已是汗流浹背办成。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工迂卢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人而克。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓员萍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親碎绎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容